@graph8/sdk 0.308.1 → 0.357.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 (654) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +1440 -21
  390. package/dist/index.d.ts +1440 -21
  391. package/dist/index.js +42 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +42 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +1340 -18
  400. package/dist/react.d.ts +1340 -18
  401. package/dist/react.js +41 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +41 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +6 -3
  611. package/docs/api/appObjects.d.ts +1 -1
  612. package/docs/api/appObjects.md +1 -1
  613. package/docs/api/by-path.md +29 -0
  614. package/docs/api/objects.d.ts +72 -5
  615. package/docs/api/objects.md +84 -7
  616. package/docs/api/processLists.d.ts +247 -0
  617. package/docs/api/processLists.md +409 -0
  618. package/docs/api/reports.d.ts +476 -0
  619. package/docs/api/reports.md +539 -0
  620. package/docs/api/scheduleBuy.d.ts +12 -8
  621. package/docs/api/scheduleBuy.md +13 -10
  622. package/docs/api/seo.d.ts +406 -3
  623. package/docs/api/seo.md +505 -1
  624. package/docs/api/workspaceBackup.d.ts +324 -0
  625. package/docs/api/workspaceBackup.md +427 -0
  626. package/package.json +174 -5
  627. package/stacks/6sense.json +46 -0
  628. package/stacks/README.md +20 -0
  629. package/stacks/apollo.json +128 -0
  630. package/stacks/clay.json +59 -0
  631. package/stacks/gong.json +72 -0
  632. package/stacks/hubspot.json +162 -0
  633. package/stacks/intercom.json +53 -0
  634. package/stacks/mailchimp.json +71 -0
  635. package/stacks/outreach.json +86 -0
  636. package/stacks/salesforce.json +94 -0
  637. package/stacks/stack.schema.json +32 -0
  638. package/stacks/starter.json +22 -0
  639. package/styles/styles.css +2 -0
  640. package/styles/theme.css +181 -0
  641. package/styles/tokens.css +284 -0
  642. package/templates/next-app/README.md.tmpl +35 -0
  643. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  644. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  645. package/templates/next-app/app/globals.css.tmpl +8 -0
  646. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  647. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  648. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  649. package/templates/next-app/dot-gitignore.tmpl +7 -0
  650. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  651. package/templates/next-app/next.config.mjs.tmpl +4 -0
  652. package/templates/next-app/package.json.tmpl +25 -0
  653. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  654. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,75 @@
1
+ # integrations
2
+
3
+ Show which CRMs and workspace tools are connected, keep a CRM in sync, and manage signed webhooks. The
4
+ Apollo / Salesforce "integrations" settings screen.
5
+
6
+ Depends on: `foundation`. Install: `npx @graph8/sdk add integrations`.
7
+
8
+ ## Components
9
+
10
+ | Component | What it does | Tiers |
11
+ |---|---|---|
12
+ | `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
+ | `SyncStatus` | One CRM: health, enable/disable continuous push and pull, job progress, manual contact push. | read, external |
14
+ | `WebhookManager` | Create, pause, rotate and delete webhooks; secret shown once; delivery log; server-side verification note. | read, external, destructive |
15
+
16
+ `external` and `destructive` actions open `ConfirmSpendDialog` first (they write to the customer's CRM,
17
+ change where data is sent, or remove a subscription).
18
+
19
+ ## The flow, in order
20
+
21
+ 1. **Connect a CRM (the human).** OAuth consent runs only in the graph8 app. `IntegrationList` links there
22
+ (`connectUrl`). The agent cannot do this step.
23
+ 2. **Read status.** `IntegrationList` -> `listCrmSyncs` (provider, status, last sync) and `listConnections`
24
+ (each connection's `connection_id` and `provider_config_key`, needed to disconnect).
25
+ 3. **Open a CRM.** `SyncStatus provider="hubspot"` -> `getCrmStatus` returns `connection_id`.
26
+ 4. **Switch syncing.** `setPushSyncEnabled` / `setPullSyncEnabled` with that `connection_id` (confirmed). The
27
+ response's `job_id` feeds `getSyncJob`, polled until the job ends (no streams).
28
+ 5. **Push on demand.** `pushContactsToCrm` with `records` in the provider's field names (`getCrmFields`
29
+ lists them). The result gives `pushed_count`, `failed_count` and per-record `errors`.
30
+ 6. **Webhooks.** `WebhookManager` -> `listWebhookEvents` -> `createWebhook` (confirmed). Store the returned
31
+ secret as `G8_WEBHOOK_SECRET`. Receive with `recipes/verify-webhook-route.ts`.
32
+
33
+ ## Verifying webhooks (server side only)
34
+
35
+ Deliveries carry `X-Studio-Signature` and `X-Studio-Timestamp`. On the server call
36
+ `g8.webhooks.constructEvent(rawBody, signature, timestamp, process.env.G8_WEBHOOK_SECRET, { toleranceSeconds: 300 })`
37
+ BEFORE parsing or trusting the body; it throws on a bad signature or a stale timestamp. The secret must never
38
+ reach the browser, which is why `WebhookManager` only displays it once at creation or rotation.
39
+
40
+ ## Prerequisites
41
+
42
+ - CRM connected via the app's OAuth screen (`where: "app"`). Verify: `GET /crm-syncs/{provider}/status` -> `connected: true`.
43
+ - 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"`).
45
+
46
+ ## Defaults (do not ask the human)
47
+
48
+ - Connect link: `https://app.graph8.com/studio/settings?tab=integrations`. Tolerance: 300 s.
49
+ - **Ask the human**: which events to subscribe to, and the endpoint URL.
50
+
51
+ ## Operations
52
+
53
+ | Operation | Method and path | Tier | Used by |
54
+ |---|---|---|---|
55
+ | `list_crm_syncs_crm_syncs_get` | GET /crm-syncs | read | IntegrationList |
56
+ | `list_connections_integrations_connections_get` | GET /integrations/connections | read | IntegrationList |
57
+ | `get_slack_status_integrations_slack_status_get` | GET /integrations/slack/status | read | IntegrationList |
58
+ | `get_google_workspace_status_integrations_google_workspace_get` | GET /integrations/google-workspace | read | IntegrationList |
59
+ | `get_roam_status_integrations_roam_status_get` | GET /integrations/roam/status | read | IntegrationList |
60
+ | `delete_connection_integrations_connections__connection_id__delete` | DELETE /integrations/connections/{connection_id} | destructive | IntegrationList |
61
+ | `get_crm_status_crm_syncs__provider__status_get` | GET /crm-syncs/{provider}/status | read | SyncStatus |
62
+ | `get_crm_fields_crm_syncs__provider__fields_get` | GET /crm-syncs/{provider}/fields | read | SyncStatus |
63
+ | `set_push_sync_enabled_integrations_crm_sync_push_enabled_put` | PUT /integrations/crm-sync/push-enabled | external | SyncStatus |
64
+ | `set_pull_sync_enabled_integrations_crm_sync_pull_enabled_put` | PUT /integrations/crm-sync/pull-enabled | external | SyncStatus |
65
+ | `get_sync_job_integrations_crm_sync_jobs__job_id__get` | GET /integrations/crm-sync/jobs/{job_id} | read | SyncStatus |
66
+ | `push_contacts_to_crm_crm_syncs__provider__contacts_push_post` | POST /crm-syncs/{provider}/contacts/push | external | SyncStatus |
67
+ | `list_webhooks_webhooks_get` | GET /webhooks | read | WebhookManager |
68
+ | `list_webhook_events_webhooks_events_get` | GET /webhooks/events | read | WebhookManager |
69
+ | `create_webhook_webhooks_post` | POST /webhooks | external | WebhookManager |
70
+ | `update_webhook_webhooks__webhook_id__patch` | PATCH /webhooks/{webhook_id} | external | WebhookManager |
71
+ | `rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post` | POST /webhooks/{webhook_id}/rotate-secret | external | WebhookManager |
72
+ | `list_webhook_deliveries_webhooks__webhook_id__deliveries_get` | GET /webhooks/{webhook_id}/deliveries | read | WebhookManager |
73
+ | `delete_webhook_webhooks__webhook_id__delete` | DELETE /webhooks/{webhook_id} | destructive | WebhookManager |
74
+
75
+ Recipes (`recipes/`): `verify-webhook-route.ts`, `register-webhook.ts`, `crm-sync-health.ts`, `push-contacts-to-crm.ts`.
@@ -0,0 +1,172 @@
1
+ {
2
+ "$schema": "../block.schema.json",
3
+ "id": "integrations",
4
+ "title": "Integrations & webhooks",
5
+ "summary": "Show CRM, Slack and Google connections, run CRM syncs, and manage signed webhooks.",
6
+ "features": [
7
+ "integration status list with connect and disconnect",
8
+ "CRM sync health, continuous push and pull, manual push",
9
+ "webhook subscriptions with secret reveal, rotation and delivery log"
10
+ ],
11
+ "dependsOn": [
12
+ "foundation"
13
+ ],
14
+ "operations": [
15
+ "list_crm_syncs_crm_syncs_get",
16
+ "list_connections_integrations_connections_get",
17
+ "get_slack_status_integrations_slack_status_get",
18
+ "get_google_workspace_status_integrations_google_workspace_get",
19
+ "get_roam_status_integrations_roam_status_get",
20
+ "delete_connection_integrations_connections__connection_id__delete",
21
+ "get_crm_status_crm_syncs__provider__status_get",
22
+ "get_crm_fields_crm_syncs__provider__fields_get",
23
+ "set_push_sync_enabled_integrations_crm_sync_push_enabled_put",
24
+ "set_pull_sync_enabled_integrations_crm_sync_pull_enabled_put",
25
+ "get_sync_job_integrations_crm_sync_jobs__job_id__get",
26
+ "push_contacts_to_crm_crm_syncs__provider__contacts_push_post",
27
+ "list_webhooks_webhooks_get",
28
+ "list_webhook_events_webhooks_events_get",
29
+ "create_webhook_webhooks_post",
30
+ "update_webhook_webhooks__webhook_id__patch",
31
+ "rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post",
32
+ "list_webhook_deliveries_webhooks__webhook_id__deliveries_get",
33
+ "delete_webhook_webhooks__webhook_id__delete"
34
+ ],
35
+ "components": [
36
+ {
37
+ "name": "IntegrationList",
38
+ "file": "components/integration-list.tsx",
39
+ "purpose": "Status of the five CRMs plus Slack, Google Workspace and Roam; connect links to the app; disconnect is confirmed.",
40
+ "operations": [
41
+ "list_crm_syncs_crm_syncs_get",
42
+ "list_connections_integrations_connections_get",
43
+ "get_slack_status_integrations_slack_status_get",
44
+ "get_google_workspace_status_integrations_google_workspace_get",
45
+ "get_roam_status_integrations_roam_status_get",
46
+ "delete_connection_integrations_connections__connection_id__delete"
47
+ ],
48
+ "tiers": [
49
+ "read",
50
+ "destructive"
51
+ ]
52
+ },
53
+ {
54
+ "name": "SyncStatus",
55
+ "file": "components/sync-status.tsx",
56
+ "purpose": "One CRM's health, continuous push/pull switches, job progress and a manual contact push. Writes are confirmed.",
57
+ "operations": [
58
+ "get_crm_status_crm_syncs__provider__status_get",
59
+ "get_crm_fields_crm_syncs__provider__fields_get",
60
+ "set_push_sync_enabled_integrations_crm_sync_push_enabled_put",
61
+ "set_pull_sync_enabled_integrations_crm_sync_pull_enabled_put",
62
+ "get_sync_job_integrations_crm_sync_jobs__job_id__get",
63
+ "push_contacts_to_crm_crm_syncs__provider__contacts_push_post"
64
+ ],
65
+ "tiers": [
66
+ "read",
67
+ "external"
68
+ ]
69
+ },
70
+ {
71
+ "name": "WebhookManager",
72
+ "file": "components/webhook-manager.tsx",
73
+ "purpose": "Create, pause, rotate and delete webhook subscriptions, show the secret once, and browse deliveries.",
74
+ "operations": [
75
+ "list_webhooks_webhooks_get",
76
+ "list_webhook_events_webhooks_events_get",
77
+ "create_webhook_webhooks_post",
78
+ "update_webhook_webhooks__webhook_id__patch",
79
+ "rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post",
80
+ "list_webhook_deliveries_webhooks__webhook_id__deliveries_get",
81
+ "delete_webhook_webhooks__webhook_id__delete"
82
+ ],
83
+ "tiers": [
84
+ "read",
85
+ "external",
86
+ "destructive"
87
+ ]
88
+ }
89
+ ],
90
+ "hooks": [],
91
+ "lib": [
92
+ {
93
+ "file": "lib/integrations-utils.ts",
94
+ "purpose": "CRM provider list and labels, status types and envelope helpers shared by the integrations components."
95
+ }
96
+ ],
97
+ "recipes": [
98
+ {
99
+ "file": "recipes/verify-webhook-route.ts",
100
+ "title": "Receive graph8 webhooks in a Next.js route and verify the signature",
101
+ "when": "After a webhook subscription exists; verification is server-side only."
102
+ },
103
+ {
104
+ "file": "recipes/register-webhook.ts",
105
+ "title": "Subscribe a URL to graph8 events",
106
+ "when": "Wiring a server to graph8 (external tier: get approval)."
107
+ },
108
+ {
109
+ "file": "recipes/crm-sync-health.ts",
110
+ "title": "Check every CRM connection is healthy",
111
+ "when": "A scheduled health check or status page."
112
+ },
113
+ {
114
+ "file": "recipes/push-contacts-to-crm.ts",
115
+ "title": "Push contacts to a connected CRM",
116
+ "when": "Sending graph8 contacts into a connected CRM (external tier: get approval)."
117
+ }
118
+ ],
119
+ "prerequisites": [
120
+ {
121
+ "id": "crm-oauth-connected",
122
+ "description": "The CRM (HubSpot, Salesforce, Pipedrive, Zoho or SugarCRM) is connected through the graph8 app's OAuth screen. The API cannot run the consent flow.",
123
+ "where": "app",
124
+ "verify": "GET /crm-syncs/{provider}/status returns connected: true."
125
+ },
126
+ {
127
+ "id": "webhook-endpoint",
128
+ "description": "A public https route in the customer app receives webhooks and verifies them with constructEvent (recipes/verify-webhook-route.ts), with G8_WEBHOOK_SECRET set.",
129
+ "where": "app",
130
+ "verify": "A test delivery returns 200 and the route rejects a tampered body with 400."
131
+ },
132
+ {
133
+ "id": "integrations-admin",
134
+ "description": "The API key's user can manage integrations (disconnect and webhook writes need admin scopes).",
135
+ "where": "api",
136
+ "verify": "GET /webhooks returns 200 and POST /webhooks is not 403."
137
+ }
138
+ ],
139
+ "scopes": [
140
+ "integrations:delete",
141
+ "integrations:read",
142
+ "integrations:run",
143
+ "webhooks:delete",
144
+ "webhooks:read",
145
+ "webhooks:run"
146
+ ],
147
+ "defaults": [
148
+ {
149
+ "decision": "Where Connect sends the person",
150
+ "default": "https://app.graph8.com/studio/settings?tab=integrations (OAuth only runs in the app)",
151
+ "askHuman": false
152
+ },
153
+ {
154
+ "decision": "Which events a webhook subscribes to",
155
+ "default": "None: pick only the events the app handles",
156
+ "askHuman": true
157
+ },
158
+ {
159
+ "decision": "Webhook endpoint URL",
160
+ "default": "https://<your-domain>/api/webhooks/graph8",
161
+ "askHuman": true
162
+ },
163
+ {
164
+ "decision": "Timestamp tolerance when verifying",
165
+ "default": "300 seconds",
166
+ "askHuman": false
167
+ }
168
+ ],
169
+ "peerDependencies": {
170
+ "react": ">=18"
171
+ }
172
+ }
@@ -0,0 +1,165 @@
1
+ "use client";
2
+
3
+ /**
4
+ * IntegrationList: every integration the workspace can use and whether it is connected: the five CRMs
5
+ * graph8 syncs with, plus Slack, Google Workspace and Roam.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listCrmSyncs GET /crm-syncs (read; provider + status per CRM)
9
+ * 2. listConnections GET /integrations/connections (read; the connection_id to disconnect)
10
+ * 3. getSlackStatus / getGoogleWorkspaceStatus / getRoamStatus GET /integrations/... (read)
11
+ * 4. deleteConnection DELETE /integrations/connections/{connection_id}?provider_config_key=
12
+ * (destructive: confirmed with ConfirmSpendDialog first)
13
+ *
14
+ * Connecting is NOT done here: an OAuth consent screen can only run in the graph8 app, so "Connect"
15
+ * links the person to it (`connectUrl`). Disconnecting keeps records already synced.
16
+ */
17
+
18
+ import { Button, Skeleton } from "@graph8/sdk/ui";
19
+ import { useState } from "react";
20
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import {
26
+ arrayOf,
27
+ CRM_PROVIDERS,
28
+ formatWhen,
29
+ GRAPH8_APP_INTEGRATIONS_URL,
30
+ looksConnected,
31
+ PROVIDER_LABELS,
32
+ } from "../lib/integrations-utils";
33
+ import type { ConnectionRow, CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
34
+
35
+ export interface IntegrationListProps {
36
+ /** Where "Connect" sends the person. Default: graph8 Settings > Integrations. */
37
+ connectUrl?: string;
38
+ /** Called when the person opens a CRM's sync panel (render SyncStatus for it). */
39
+ onOpenSync?: (provider: CrmProvider) => void;
40
+ className?: string;
41
+ }
42
+
43
+ /** Only these mean "working". Anything else (not_connected, inactive, pending, error...) is not connected. */
44
+ const CONNECTED_STATES = ["connected", "active", "ok", "healthy", "syncing", "synced", "enabled", "linked"];
45
+
46
+ const CHAT_TOOLS = [
47
+ { key: "slack", label: "Slack", path: "/integrations/slack/status" },
48
+ { key: "google", label: "Google Workspace", path: "/integrations/google-workspace" },
49
+ { key: "roam", label: "Roam", path: "/integrations/roam/status" },
50
+ ] as const;
51
+
52
+ function Pill({ on, label }: { on: boolean; label?: string }) {
53
+ 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"}`}>
55
+ {label ?? (on ? "Connected" : "Not connected")}
56
+ </span>
57
+ );
58
+ }
59
+
60
+ function ChatTools() {
61
+ const state = useG8Query(async (signal) => {
62
+ const results = await Promise.allSettled(CHAT_TOOLS.map((t) => g8Get(t.path, undefined, { signal })));
63
+ return CHAT_TOOLS.map((t, i) => ({
64
+ ...t,
65
+ connected: results[i].status === "fulfilled" && looksConnected((results[i] as PromiseFulfilledResult<unknown>).value),
66
+ }));
67
+ }, []);
68
+ if (state.loading && !state.data) return <Skeleton className="h-10 w-full" />;
69
+ return (
70
+ <ul className="divide-y rounded border border-gray-200" aria-label="Workspace tools">
71
+ {(state.data ?? []).map((t) => (
72
+ <li key={t.key} className="flex items-center justify-between px-3 py-2 text-sm">
73
+ <span>{t.label}</span>
74
+ <Pill on={t.connected} />
75
+ </li>
76
+ ))}
77
+ </ul>
78
+ );
79
+ }
80
+
81
+ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOpenSync, className }: IntegrationListProps) {
82
+ const [disconnecting, setDisconnecting] = useState<{ provider: CrmProvider; connection: ConnectionRow } | null>(null);
83
+ const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
84
+ const connections = useG8Query((signal) => g8Get("/integrations/connections", undefined, { signal }), []);
85
+ const remove = useG8Mutation(async (c: ConnectionRow) => {
86
+ await g8Delete(`/integrations/connections/${c.connection_id}`, { query: { provider_config_key: c.provider_config_key } });
87
+ await Promise.all([syncs.refetch(), connections.refetch()]);
88
+ });
89
+
90
+ const rows = arrayOf<CrmSyncRow>(syncs.data);
91
+ const conns = arrayOf<ConnectionRow>(connections.data);
92
+ const byProvider = (p: CrmProvider) => rows.find((r) => r.provider?.toLowerCase() === p);
93
+ // Exact provider match only. When there is more than one connection for a provider we cannot know which one
94
+ // the person means, so Disconnect is withheld and they are sent to the app.
95
+ const connectionsFor = (p: CrmProvider) =>
96
+ conns.filter((c) => (c.provider ?? "").toLowerCase() === p || (c.provider_config_key ?? "").toLowerCase().split(/[-_]/)[0] === p);
97
+ const connectionFor = (p: CrmProvider) => {
98
+ const all = connectionsFor(p);
99
+ const wanted = byProvider(p)?.connection_id;
100
+ return all.find((c) => wanted && c.connection_id === wanted) ?? (all.length === 1 ? all[0] : undefined);
101
+ };
102
+
103
+ if (syncs.error) return <ApiError error={syncs.error} onRetry={() => void syncs.refetch()} className={className} />;
104
+ if (syncs.loading && !syncs.data) return <Skeleton className={`h-40 w-full ${className ?? ""}`} />;
105
+
106
+ return (
107
+ <div className={`space-y-4 ${className ?? ""}`}>
108
+ <ul className="divide-y rounded border border-gray-200" aria-label="CRM integrations">
109
+ {CRM_PROVIDERS.map((p) => {
110
+ const row = byProvider(p);
111
+ const connected = Boolean(row && CONNECTED_STATES.includes(row.status?.toLowerCase()));
112
+ const connection = connectionFor(p);
113
+ return (
114
+ <li key={p} className="flex flex-wrap items-center justify-between gap-2 px-3 py-3" data-provider={p}>
115
+ <div>
116
+ <div className="font-medium">{PROVIDER_LABELS[p]}</div>
117
+ <div className="text-xs text-gray-500">
118
+ {connected ? `Last sync: ${formatWhen(row?.last_sync_at)}` : "Connect to sync contacts and companies."}
119
+ </div>
120
+ </div>
121
+ <div className="flex items-center gap-2">
122
+ <Pill on={connected} label={row?.status && !connected ? row.status : undefined} />
123
+ {connected ? (
124
+ <>
125
+ <Button size="sm" variant="outline" onClick={() => onOpenSync?.(p)}>
126
+ Manage sync
127
+ </Button>
128
+ <Button
129
+ size="sm"
130
+ variant="ghost"
131
+ disabled={!connection?.connection_id || !connection.provider_config_key}
132
+ onClick={() => connection && setDisconnecting({ provider: p, connection })}
133
+ >
134
+ Disconnect
135
+ </Button>
136
+ </>
137
+ ) : (
138
+ <Button size="sm" asChild>
139
+ <a href={connectUrl} target="_blank" rel="noreferrer">
140
+ Connect in graph8
141
+ </a>
142
+ </Button>
143
+ )}
144
+ </div>
145
+ </li>
146
+ );
147
+ })}
148
+ </ul>
149
+ {remove.error ? <ApiError error={remove.error} onRetry={remove.reset} /> : null}
150
+ <ChatTools />
151
+ {disconnecting ? (
152
+ <ConfirmSpendDialog
153
+ open
154
+ tier="destructive"
155
+ title={`Disconnect ${PROVIDER_LABELS[disconnecting.provider]}`}
156
+ description="Syncs configured against this connection stop working. Records already synced stay."
157
+ confirmLabel="Disconnect"
158
+ confirming={remove.loading}
159
+ onCancel={() => setDisconnecting(null)}
160
+ onConfirm={() => void remove.mutate(disconnecting.connection).then(() => setDisconnecting(null))}
161
+ />
162
+ ) : null}
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,223 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SyncStatus: the health of one CRM connection, its continuous sync switches and a manual push.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. getCrmStatus GET /crm-syncs/{provider}/status (read; connected? connection_id)
8
+ * 2. getCrmFields GET /crm-syncs/{provider}/fields?entity_type (read; the CRM's field list)
9
+ * 3. setPushSyncEnabled PUT /integrations/crm-sync/push-enabled (external: confirmed)
10
+ * setPullSyncEnabled PUT /integrations/crm-sync/pull-enabled (external: confirmed)
11
+ * 4. getSyncJob GET /integrations/crm-sync/jobs/{job_id} (read; polled until the job ends)
12
+ * 5. pushContactsToCrm POST /crm-syncs/{provider}/contacts/push (external: confirmed with the count)
13
+ *
14
+ * Tiers: 3 and 5 write to the customer's CRM, so each opens ConfirmSpendDialog first. The `connection_id`
15
+ * from step 1 feeds step 3; the `job_id` that step 3 returns feeds step 4. The API has no read for the
16
+ * current on/off state, so the switches are explicit Enable / Disable buttons (idempotent).
17
+ */
18
+
19
+ import { useEffect, useState } from "react";
20
+ import { Button, Skeleton } from "@graph8/sdk/ui";
21
+ import { g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { formatWhen, PROVIDER_LABELS, TERMINAL_JOB_STATES } from "../lib/integrations-utils";
28
+ import type { CrmProvider, CrmStatus } from "../lib/integrations-utils";
29
+
30
+ export interface SyncStatusProps {
31
+ provider: CrmProvider;
32
+ /** Contact records (in the provider's field names) to offer a manual push for. */
33
+ records?: ReadonlyArray<Record<string, unknown>>;
34
+ className?: string;
35
+ }
36
+
37
+ type Direction = "push" | "pull";
38
+ type Pending = { kind: "switch"; direction: Direction; enabled: boolean } | { kind: "push" };
39
+
40
+ interface JobStatus {
41
+ job_id?: string | null;
42
+ status?: string | null;
43
+ }
44
+
45
+ function JobProgress({ jobId }: { jobId: string }) {
46
+ const job = useJobPoll<JobStatus>(
47
+ async (signal) => (await g8Get<JobStatus>(`/integrations/crm-sync/jobs/${jobId}`, undefined, { signal })).data,
48
+ {
49
+ isTerminal: (j) => TERMINAL_JOB_STATES.includes(String(j.status ?? "").toLowerCase()),
50
+ },
51
+ );
52
+ return (
53
+ <p role="status" className="text-sm text-gray-700" data-testid="sync-job">
54
+ Job {jobId}: {job.status?.status ?? "starting"}
55
+ {job.polling ? "..." : ""}
56
+ {job.error ? " (could not read the job status)" : ""}
57
+ </p>
58
+ );
59
+ }
60
+
61
+ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
62
+ const [pending, setPending] = useState<Pending | null>(null);
63
+ const [jobId, setJobId] = useState<string | null>(null);
64
+ const [pushed, setPushed] = useState<string | null>(null);
65
+ const [showFields, setShowFields] = useState(false);
66
+ const label = PROVIDER_LABELS[provider];
67
+ useEffect(() => {
68
+ // A different CRM is a different context: drop the previous one's job, result and pending confirmation.
69
+ setJobId(null);
70
+ setPushed(null);
71
+ setPending(null);
72
+ setShowFields(false);
73
+ }, [provider]);
74
+
75
+ // Tag the answer with the provider it was fetched for: useG8Query keeps the previous data while a refetch runs,
76
+ // and a write must never carry another CRM's connection_id.
77
+ const statusQuery = useG8Query(
78
+ async (signal) => ({ provider, res: await g8Get<CrmStatus>(`/crm-syncs/${provider}/status`, undefined, { signal }) }),
79
+ [provider],
80
+ );
81
+ const status = {
82
+ ...statusQuery,
83
+ data: statusQuery.data?.provider === provider ? statusQuery.data.res : undefined,
84
+ refetch: statusQuery.refetch,
85
+ };
86
+ const fields = useG8Query(
87
+ async (signal) =>
88
+ (
89
+ await g8Get<{ fields?: Array<{ name?: string; label?: string }> }>(
90
+ `/crm-syncs/${provider}/fields`,
91
+ { entity_type: "contact" },
92
+ { signal },
93
+ )
94
+ ).data?.fields ?? [],
95
+ [provider],
96
+ { enabled: showFields },
97
+ );
98
+ const connectionId = status.data?.data?.connection_id ?? undefined;
99
+
100
+ const run = useG8Mutation(async (p: Pending) => {
101
+ if (p.kind === "switch") {
102
+ const path = p.direction === "push" ? "/integrations/crm-sync/push-enabled" : "/integrations/crm-sync/pull-enabled";
103
+ const res = await g8Put<JobStatus>(path, { connection_id: connectionId, enabled: p.enabled });
104
+ setJobId(res.data?.job_id ?? null);
105
+ } else {
106
+ const res = await g8Post<{ pushed_count?: number; failed_count?: number }>(`/crm-syncs/${provider}/contacts/push`, {
107
+ records: records ?? [],
108
+ });
109
+ setPushed(`${res.data?.pushed_count ?? 0} pushed, ${res.data?.failed_count ?? 0} failed`);
110
+ }
111
+ });
112
+
113
+ if (status.error) return <ApiError error={status.error} onRetry={() => void status.refetch()} className={className} />;
114
+ if (status.loading && !status.data) return <Skeleton className={`h-32 w-full ${className ?? ""}`} />;
115
+ const s = status.data?.data;
116
+ if (!s?.connected) {
117
+ return (
118
+ <p role="status" className={`rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 ${className ?? ""}`}>
119
+ {label} is not connected. {s?.message ?? "Connect it in graph8, then come back."}
120
+ </p>
121
+ );
122
+ }
123
+
124
+ const confirmCopy =
125
+ pending?.kind === "push"
126
+ ? {
127
+ title: `Push ${records?.length ?? 0} contacts to ${label}`,
128
+ description: "Creates or updates these contacts in your CRM.",
129
+ label: "Push contacts",
130
+ }
131
+ : pending
132
+ ? {
133
+ title: `${pending.enabled ? "Turn on" : "Turn off"} continuous ${pending.direction} with ${label}`,
134
+ description: pending.enabled
135
+ ? `graph8 will keep ${pending.direction === "push" ? "writing contacts to" : "reading contacts from"} your CRM.`
136
+ : "Syncing in this direction stops.",
137
+ label: pending.enabled ? "Turn on" : "Turn off",
138
+ }
139
+ : null;
140
+
141
+ return (
142
+ <div className={`space-y-4 ${className ?? ""}`}>
143
+ <div className="flex items-center justify-between">
144
+ <div>
145
+ <h3 className="font-medium">{label}</h3>
146
+ <p className="text-xs text-gray-500">Connected. Last sync: {formatWhen(s.last_sync_at)}</p>
147
+ </div>
148
+ <Button size="sm" variant="outline" onClick={() => void status.refetch()}>
149
+ Recheck
150
+ </Button>
151
+ </div>
152
+
153
+ {(["push", "pull"] as const).map((direction) => (
154
+ <div key={direction} className="flex items-center justify-between rounded border border-gray-200 px-3 py-2 text-sm">
155
+ <span>{direction === "push" ? "Send graph8 contacts to the CRM" : "Bring CRM contacts into graph8"}</span>
156
+ <span className="flex gap-2">
157
+ <Button
158
+ size="sm"
159
+ variant="outline"
160
+ disabled={!connectionId || statusQuery.loading}
161
+ onClick={() => setPending({ kind: "switch", direction, enabled: true })}
162
+ >
163
+ Enable {direction}
164
+ </Button>
165
+ <Button
166
+ size="sm"
167
+ variant="ghost"
168
+ disabled={!connectionId || statusQuery.loading}
169
+ onClick={() => setPending({ kind: "switch", direction, enabled: false })}
170
+ >
171
+ Disable {direction}
172
+ </Button>
173
+ </span>
174
+ </div>
175
+ ))}
176
+
177
+ {jobId ? <JobProgress key={jobId} jobId={jobId} /> : null}
178
+ {records && records.length > 0 ? (
179
+ <Button onClick={() => setPending({ kind: "push" })}>Push {records.length} contacts now</Button>
180
+ ) : null}
181
+ {pushed ? (
182
+ <p role="status" className="text-sm text-green-700">
183
+ {pushed}
184
+ </p>
185
+ ) : null}
186
+ {run.error ? <ApiError error={run.error} onRetry={run.reset} /> : null}
187
+
188
+ <div>
189
+ <Button size="sm" variant="link" className="px-0" onClick={() => setShowFields((v) => !v)}>
190
+ {showFields ? "Hide" : "Show"} contact fields
191
+ </Button>
192
+ {showFields ? (
193
+ fields.loading ? (
194
+ <Skeleton className="h-8 w-full" />
195
+ ) : fields.error ? (
196
+ <ApiError error={fields.error} />
197
+ ) : (
198
+ <ul className="mt-1 flex flex-wrap gap-1 text-xs">
199
+ {(fields.data ?? []).map((f, i) => (
200
+ <li key={i} className="rounded border border-gray-200 px-1.5 py-0.5">
201
+ {f.label ?? f.name}
202
+ </li>
203
+ ))}
204
+ </ul>
205
+ )
206
+ ) : null}
207
+ </div>
208
+
209
+ {pending && confirmCopy ? (
210
+ <ConfirmSpendDialog
211
+ open
212
+ tier="external"
213
+ title={confirmCopy.title}
214
+ description={confirmCopy.description}
215
+ confirmLabel={confirmCopy.label}
216
+ confirming={run.loading}
217
+ onCancel={() => setPending(null)}
218
+ onConfirm={() => void run.mutate(pending).then(() => setPending(null))}
219
+ />
220
+ ) : null}
221
+ </div>
222
+ );
223
+ }