@graph8/sdk 0.370.0 → 0.374.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -54,7 +54,7 @@ const CHAT_TOOLS = [
54
54
 
55
55
  function Pill({ on, label }: { on: boolean; label?: string }) {
56
56
  return (
57
- <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"}`}>
58
58
  {label ?? (on ? "Connected" : "Not connected")}
59
59
  </span>
60
60
  );
@@ -70,7 +70,7 @@ function ChatTools() {
70
70
  }, []);
71
71
  if (state.loading && !state.data) return <Skeleton className="h-10 w-full" />;
72
72
  return (
73
- <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">
74
74
  {(state.data ?? []).map((t) => (
75
75
  <li key={t.key} className="flex items-center justify-between px-3 py-2 text-sm">
76
76
  <span>{t.label}</span>
@@ -110,7 +110,7 @@ export function IntegrationList({ connectUrl: connectUrlProp, onOpenSync, onSele
110
110
 
111
111
  return (
112
112
  <div className={`space-y-4 ${className ?? ""}`}>
113
- <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">
114
114
  {CRM_PROVIDERS.map((p) => {
115
115
  const row = byProvider(p);
116
116
  const connected = Boolean(row && CONNECTED_STATES.includes(row.status?.toLowerCase()));
@@ -119,7 +119,7 @@ export function IntegrationList({ connectUrl: connectUrlProp, onOpenSync, onSele
119
119
  <li key={p} className="flex flex-wrap items-center justify-between gap-2 px-3 py-3" data-provider={p}>
120
120
  <div>
121
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-gray-500">
122
+ <div className="text-xs text-muted-foreground">
123
123
  {connected ? `Last sync: ${formatWhen(row?.last_sync_at)}` : "Connect to sync contacts and companies."}
124
124
  </div>
125
125
  </div>
@@ -8,8 +8,15 @@
8
8
  * Webhooks create webhooks (WebhookManager); each opens its complete detail screen (WebhookDetail: every field, edit, deliveries,
9
9
  * pause, rotate secret, delete; `?id=`). Every detail also opens as a full page with `?view=full`.
10
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
+ *
11
17
  * SDK operations: two reads for the stat strip (GET /crm-syncs, GET /webhooks). Composes IntegrationList (read +
12
- * destructive, confirmed), SyncStatus (external, confirmed) and WebhookManager (external + destructive, confirmed).
18
+ * destructive, confirmed), SyncStatus and SyncTriggers (external, confirmed), WebhookManager (external + destructive, confirmed),
19
+ * WorkspaceTools and AdAccounts (reads).
13
20
  * Prerequisites: a CRM connected in the app (where: "app"); a public https route with G8_WEBHOOK_SECRET for webhooks.
14
21
  */
15
22
  import type { ReactNode } from "react";
@@ -24,7 +31,9 @@ import { SplitView } from "@/g8/components/screen/split-view";
24
31
  import { SectionCard } from "@/g8/components/screen/section-card";
25
32
  import { arrayOf, CRM_PROVIDERS, GRAPH8_APP_INTEGRATIONS_PATH, PROVIDER_LABELS } from "../lib/integrations-utils";
26
33
  import type { CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
34
+ import { AdAccounts } from "./ad-accounts";
27
35
  import { IntegrationList } from "./integration-list";
36
+ import { WorkspaceTools } from "./workspace-tools";
28
37
  import { CrmDetail } from "./crm-detail";
29
38
  import { WebhookDetail } from "./webhook-detail";
30
39
  import { WebhookManager } from "./webhook-manager";
@@ -67,13 +76,15 @@ export function IntegrationsScreen(props: IntegrationsScreenProps) {
67
76
  const brand = useBrand();
68
77
  const connectUrl = props.connectUrl ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
69
78
  const [tabValue, setTab] = useUrlState("tab", "connections");
70
- const tab = tabValue === "webhooks" ? "webhooks" : "connections";
79
+ const tab = ["webhooks", "tools", "ads"].includes(tabValue) ? tabValue : "connections";
71
80
  const [id, setId] = useUrlState("id", "");
72
81
  const [view, setView] = useUrlState("view", "");
82
+ const [, setMigration] = useUrlState("migration", "");
73
83
  const expanded = view === "full" && Boolean(id);
74
84
  const [tick, setTick] = useState(0);
75
85
  const stats = useStats();
76
86
  const close = () => {
87
+ setMigration("");
77
88
  setId("");
78
89
  setView("");
79
90
  };
@@ -101,6 +112,8 @@ export function IntegrationsScreen(props: IntegrationsScreenProps) {
101
112
  tabs={[
102
113
  { id: "connections", label: "Connections" },
103
114
  { id: "webhooks", label: "Webhooks" },
115
+ { id: "tools", label: "Tools" },
116
+ { id: "ads", label: "Ad accounts" },
104
117
  ]}
105
118
  activeTab={tab}
106
119
  onTabChange={(next) => {
@@ -118,11 +131,15 @@ export function IntegrationsScreen(props: IntegrationsScreenProps) {
118
131
  </a>
119
132
  </SectionCard>
120
133
  {split(
121
- <IntegrationList key={tick} connectUrl={connectUrl} onSelectCrm={(p) => setId(p)} onOpenSync={(p) => setId(p)} />,
134
+ <IntegrationList key={tick} connectUrl={connectUrl} onSelectCrm={(p) => { setMigration(""); setId(p); }} onOpenSync={(p) => { setMigration(""); setId(p); }} />,
122
135
  provider ? <CrmDetail key={provider} provider={provider} connectUrl={connectUrl} records={props.pushRecords} {...panel} {...slots({ kind: "crm", provider })} /> : null,
123
136
  provider != null,
124
137
  )}
125
138
  </div>
139
+ ) : tab === "tools" ? (
140
+ <WorkspaceTools connectUrl={connectUrl} />
141
+ ) : tab === "ads" ? (
142
+ <AdAccounts connectUrl={connectUrl} />
126
143
  ) : (
127
144
  split(
128
145
  <WebhookManager key={tick} defaultUrl={props.defaultWebhookUrl} onSelectWebhook={(h) => setId(h.id)} />,
@@ -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
+ }
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SugarCrmConnect: connect SugarCRM from here. Unlike HubSpot, Salesforce, Pipedrive and Zoho (whose sign-in screen only runs in the graph8
5
+ * app, so those link out), SugarCRM signs in with an instance address, a user and a password, which the API can take directly.
6
+ * Not connected the form; Connect signs in and stores the credentials (there is no way to check them before saving).
7
+ * Connected "Check connection" (free, reads the stored login), "Update credentials" (sign in again with new ones), "Remove".
8
+ * Connecting and updating sign in to your SugarCRM and store the credentials, so they go through ConfirmSpendDialog (external);
9
+ * removing is destructive and confirmed. The password is sent once through your server's proxy and is cleared from the form afterwards.
10
+ *
11
+ * SDK operations, in order:
12
+ * 1. validateSugarcrm POST /integrations/sugarcrm/validate (read) checks the stored credentials
13
+ * 2. connectSugarcrm POST /integrations/sugarcrm/connect { instance_url, username, password, client_id?, client_secret? } (external, confirmed)
14
+ * 3. reconnectSugarcrm POST /integrations/sugarcrm/reconnect { same } (external, confirmed)
15
+ * 4. disconnectSugarcrm DELETE /integrations/sugarcrm (destructive, confirmed)
16
+ */
17
+ import { useState } from "react";
18
+ import { Button, Input, Label } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { g8Delete, g8Post } from "@/g8/lib/g8-client";
23
+ import { useBrand } from "@/g8/hooks/use-brand";
24
+
25
+ export interface SugarCrmConnectProps {
26
+ /** Whether SugarCRM is connected now (from the sync list). */
27
+ connected: boolean;
28
+ /** After a connect, update or removal. */
29
+ onChanged?: () => void;
30
+ className?: string;
31
+ }
32
+
33
+ interface Form { instance_url: string; username: string; password: string; client_id: string; client_secret: string }
34
+ const BLANK: Form = { instance_url: "", username: "", password: "", client_id: "", client_secret: "" };
35
+ type Ask = "connect" | "reconnect" | "remove";
36
+
37
+ /** The request body: client id and secret only when typed (the instance's defaults apply otherwise). */
38
+ export function sugarBody(f: Form): Record<string, string> {
39
+ return { instance_url: f.instance_url.trim().replace(/\/+$/, ""), username: f.username.trim(), password: f.password, ...(f.client_id.trim() ? { client_id: f.client_id.trim() } : {}), ...(f.client_secret ? { client_secret: f.client_secret } : {}) };
40
+ }
41
+
42
+ export function SugarCrmConnect({ connected, onChanged, className }: SugarCrmConnectProps) {
43
+ const brand = useBrand();
44
+ const [f, setF] = useState<Form>(BLANK);
45
+ const [ask, setAsk] = useState<Ask | null>(null);
46
+ const [showAdvanced, setShowAdvanced] = useState(false);
47
+ const [editing, setEditing] = useState(false);
48
+ const set = (patch: Partial<Form>) => setF((x) => ({ ...x, ...patch }));
49
+ const valid = /^https?:\/\/\S+$/i.test(f.instance_url.trim()) && f.username.trim() && f.password;
50
+ const done = () => { setF(BLANK); setAsk(null); setEditing(false); onChanged?.(); };
51
+
52
+ const connect = useG8Mutation((kind: "connect" | "reconnect") => g8Post(`/integrations/sugarcrm/${kind}`, sugarBody(f)), { onSuccess: done, onError: () => setAsk(null) });
53
+ const check = useG8Mutation(() => g8Post("/integrations/sugarcrm/validate", {}));
54
+ const remove = useG8Mutation(() => g8Delete("/integrations/sugarcrm"), { onSuccess: done, onError: () => setAsk(null) });
55
+ const verdict = (check.data as { data?: { valid?: boolean; connected?: boolean; message?: string } } | undefined)?.data;
56
+ const showForm = !connected || editing;
57
+
58
+ return (
59
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
60
+ {connected ? (
61
+ <div className="flex flex-wrap items-center gap-2">
62
+ <Button size="sm" variant="outline" loading={check.loading} onClick={() => void check.mutate(undefined)}>Check connection</Button>
63
+ <Button size="sm" variant="outline" onClick={() => setEditing((v) => !v)}>{editing ? "Close" : "Update credentials"}</Button>
64
+ <Button size="sm" variant="ghost" onClick={() => setAsk("remove")}>Remove</Button>
65
+ {check.data !== undefined ? <span role="status" className="text-sm" data-testid="sugar-check">{verdict?.valid === false || verdict?.connected === false ? `Sign-in failed${verdict?.message ? `: ${verdict.message}` : ""}` : "Signed in to SugarCRM."}</span> : null}
66
+ </div>
67
+ ) : <p className="text-sm text-muted-foreground">Sign in to your SugarCRM instance to sync contacts, companies and deals.</p>}
68
+ {check.error ? <ApiError error={check.error} title="Could not check the connection" /> : null}
69
+ {showForm ? (
70
+ <form className="flex flex-col gap-3" onSubmit={(e) => { e.preventDefault(); if (valid) setAsk(connected ? "reconnect" : "connect"); }}>
71
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-url">Instance address</Label><Input id="sugar-url" type="url" placeholder="https://yourcompany.sugarondemand.com" value={f.instance_url} onChange={(e) => set({ instance_url: e.target.value })} /></div>
72
+ <div className="grid gap-3 sm:grid-cols-2">
73
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-user">User</Label><Input id="sugar-user" autoComplete="off" value={f.username} onChange={(e) => set({ username: e.target.value })} /></div>
74
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-pass">Password</Label><Input id="sugar-pass" type="password" autoComplete="new-password" value={f.password} onChange={(e) => set({ password: e.target.value })} /></div>
75
+ </div>
76
+ <button type="button" className="self-start text-xs underline" onClick={() => setShowAdvanced((v) => !v)}>{showAdvanced ? "Hide" : "Show"} client credentials</button>
77
+ {showAdvanced ? (
78
+ <div className="grid gap-3 sm:grid-cols-2">
79
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-cid">Client id (optional)</Label><Input id="sugar-cid" autoComplete="off" value={f.client_id} onChange={(e) => set({ client_id: e.target.value })} /></div>
80
+ <div className="flex flex-col gap-1"><Label htmlFor="sugar-secret">Client secret (optional)</Label><Input id="sugar-secret" type="password" autoComplete="new-password" value={f.client_secret} onChange={(e) => set({ client_secret: e.target.value })} /></div>
81
+ </div>
82
+ ) : null}
83
+ {connect.error ? <ApiError error={connect.error} title={connected ? "Could not update the credentials" : "Could not connect SugarCRM"} /> : null}
84
+ <div><Button type="submit" disabled={!valid}>{connected ? "Save credentials" : "Connect SugarCRM"}</Button></div>
85
+ </form>
86
+ ) : null}
87
+ {remove.error ? <ApiError error={remove.error} title="Could not remove the connection" /> : null}
88
+ <ConfirmSpendDialog open={ask === "connect" || ask === "reconnect"} tier="external" title={ask === "reconnect" ? "Update the SugarCRM credentials" : "Connect SugarCRM"}
89
+ description={`${brand.name} signs in to ${f.instance_url.trim() || "your instance"} as ${f.username.trim() || "this user"} and stores the credentials to sync on your behalf.`}
90
+ confirmLabel={ask === "reconnect" ? "Update" : "Connect"} confirming={connect.loading} onConfirm={() => ask && ask !== "remove" && void connect.mutate(ask)} onCancel={() => setAsk(null)} />
91
+ <ConfirmSpendDialog open={ask === "remove"} tier="destructive" title="Remove the SugarCRM connection" description="Syncs configured against it stop working and the stored credentials are deleted. Records already synced stay."
92
+ confirmLabel="Remove" confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setAsk(null)} />
93
+ </div>
94
+ );
95
+ }
@@ -51,7 +51,7 @@ function JobProgress({ jobId }: { jobId: string }) {
51
51
  },
52
52
  );
53
53
  return (
54
- <p role="status" className="text-sm text-gray-700" data-testid="sync-job">
54
+ <p role="status" className="text-sm text-foreground" data-testid="sync-job">
55
55
  Job {jobId}: {job.status?.status ?? "starting"}
56
56
  {job.polling ? "..." : ""}
57
57
  {job.error ? " (could not read the job status)" : ""}
@@ -117,7 +117,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
117
117
  const s = status.data?.data;
118
118
  if (!s?.connected) {
119
119
  return (
120
- <p role="status" className={`rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 ${className ?? ""}`}>
120
+ <p role="status" className={`rounded border border-warning-foreground/25 bg-warning p-3 text-sm text-warning-foreground ${className ?? ""}`}>
121
121
  {label} is not connected. {s?.message ?? `Connect it in ${brand.linkLabel}, then come back.`}
122
122
  </p>
123
123
  );
@@ -145,7 +145,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
145
145
  <div className="flex items-center justify-between">
146
146
  <div>
147
147
  <h3 className="font-medium">{label}</h3>
148
- <p className="text-xs text-gray-500">Connected. Last sync: {formatWhen(s.last_sync_at)}</p>
148
+ <p className="text-xs text-muted-foreground">Connected. Last sync: {formatWhen(s.last_sync_at)}</p>
149
149
  </div>
150
150
  <Button size="sm" variant="outline" onClick={() => void status.refetch()}>
151
151
  Recheck
@@ -153,7 +153,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
153
153
  </div>
154
154
 
155
155
  {(["push", "pull"] as const).map((direction) => (
156
- <div key={direction} className="flex items-center justify-between rounded border border-gray-200 px-3 py-2 text-sm">
156
+ <div key={direction} className="flex items-center justify-between rounded border border-border px-3 py-2 text-sm">
157
157
  <span>{direction === "push" ? `Send ${brand.linkLabel} contacts to the CRM` : `Bring CRM contacts into ${brand.linkLabel}`}</span>
158
158
  <span className="flex gap-2">
159
159
  <Button
@@ -181,7 +181,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
181
181
  <Button onClick={() => setPending({ kind: "push" })}>Push {records.length} contacts now</Button>
182
182
  ) : null}
183
183
  {pushed ? (
184
- <p role="status" className="text-sm text-green-700">
184
+ <p role="status" className="text-sm text-positive-foreground">
185
185
  {pushed}
186
186
  </p>
187
187
  ) : null}
@@ -199,7 +199,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
199
199
  ) : (
200
200
  <ul className="mt-1 flex flex-wrap gap-1 text-xs">
201
201
  {(fields.data ?? []).map((f, i) => (
202
- <li key={i} className="rounded border border-gray-200 px-1.5 py-0.5">
202
+ <li key={i} className="rounded border border-border px-1.5 py-0.5">
203
203
  {f.label ?? f.name}
204
204
  </li>
205
205
  ))}
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SyncTriggers: run a sync now instead of waiting for the continuous one. For one connected CRM:
5
+ * Contacts send every graph8 contact to the CRM, or bring the CRM's contacts in (optionally only the first N).
6
+ * Deals the same for deals.
7
+ * Leads Salesforce only (the lead routes are Salesforce-specific). Check what a push would send (a free preview), choose which lead
8
+ * status qualified and unqualified contacts get, then send leads. A large push needs an explicit acknowledgement of the previewed
9
+ * count (large_population_confirmed). Bringing leads in runs the organisation's SAVED lead pull settings (only connection_id and
10
+ * an optional limit are sent, so the saved scope is not overridden).
11
+ * Each sync is a background job: it appears under "Jobs" and is followed (polled) until it ends. Every sync changes data in the CRM
12
+ * or in graph8 and can touch thousands of records, so each one goes through ConfirmSpendDialog (external) that states its scope.
13
+ *
14
+ * SDK operations, in order:
15
+ * 1. pushAllContacts / pullAllContacts POST /integrations/crm-sync/contacts/push|pull { connection_id, limit? } (external, confirmed)
16
+ * 2. pushAllDeals / pullAllDeals POST /integrations/crm-sync/deals/push|pull { connection_id, limit? } (external, confirmed)
17
+ * 3. listLeadStatuses GET /integrations/crm-sync/leads/statuses?connection_id (read)
18
+ * 4. previewLeadPush GET /integrations/crm-sync/leads/push-preview?connection_id&source_mode&... (read)
19
+ * 5. pushAllLeads / pullAllLeads POST /integrations/crm-sync/leads/push|pull { connection_id, source_mode, ... } (external, confirmed)
20
+ * 6. getSyncJob GET /integrations/crm-sync/jobs/{job_id} (read, polled)
21
+ * Syncing a graph8 list or audience needs a list picker and is started from the Lists screen.
22
+ */
23
+ import { useEffect, useState } from "react";
24
+ import { Button, Input, Label, 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 { g8Get, g8Post } from "@/g8/lib/g8-client";
33
+ import { PROVIDER_LABELS, TERMINAL_JOB_STATES } from "../lib/integrations-utils";
34
+ import { useBrand } from "@/g8/hooks/use-brand";
35
+ import type { CrmProvider } from "../lib/integrations-utils";
36
+
37
+ export interface SyncTriggersProps {
38
+ connectionId: string;
39
+ provider: CrmProvider;
40
+ className?: string;
41
+ }
42
+
43
+ interface Job { id: string; label: string }
44
+ interface Plan { path: string; label: string; scope: string; body: Record<string, unknown> }
45
+ interface JobStatus { job_id?: string | null; status?: string | null; processed?: number | null; total?: number | null; failed?: number | null }
46
+
47
+ function JobRow({ job }: { job: Job }) {
48
+ const poll = useJobPoll<JobStatus>(async (signal) => (await g8Get<JobStatus>(`/integrations/crm-sync/jobs/${encodeURIComponent(job.id)}`, undefined, { signal })).data, { isTerminal: (j) => TERMINAL_JOB_STATES.includes(String(j.status ?? "").toLowerCase()) });
49
+ const st = String(poll.status?.status ?? "starting");
50
+ const s = poll.status;
51
+ return (
52
+ <li className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm" data-testid={`job-${job.id}`}>
53
+ <span>{job.label}<span className="ml-2 text-xs text-muted-foreground">{job.id}</span></span>
54
+ <span className="flex items-center gap-2">
55
+ {s?.total ? <span className="text-xs text-muted-foreground">{s.processed ?? 0} of {s.total}{s.failed ? `, ${s.failed} failed` : ""}</span> : null}
56
+ <StatusPill tone={toneForStatus(st)} dot>{st}</StatusPill>{poll.polling ? <span className="text-xs text-muted-foreground">updating</span> : null}{poll.error ? <span className="text-xs text-error-foreground">could not read the status</span> : null}
57
+ </span>
58
+ </li>
59
+ );
60
+ }
61
+
62
+ export function SyncTriggers({ connectionId, provider, className }: SyncTriggersProps) {
63
+ const brand = useBrand();
64
+ const label = PROVIDER_LABELS[provider];
65
+ const [bigOk, setBigOk] = useState(false);
66
+ const [limit, setLimit] = useState("");
67
+ const [mode, setMode] = useState<"all_qualified" | "all_contacts">("all_qualified");
68
+ const [qualified, setQualified] = useState("");
69
+ const [unqualified, setUnqualified] = useState("");
70
+ const [plan, setPlan] = useState<Plan | null>(null);
71
+ const [jobs, setJobs] = useState<Job[]>([]);
72
+ useEffect(() => { setPlan(null); setJobs([]); setLimit(""); setQualified(""); setUnqualified(""); setBigOk(false); }, [connectionId]);
73
+
74
+ const statuses = useG8Query((signal) => g8Get("/integrations/crm-sync/leads/statuses", { connection_id: connectionId }, { signal }), [connectionId], { enabled: provider === "salesforce" });
75
+ const statusRows = (((statuses.data as { data?: { statuses?: Array<string | { value?: string; id?: string; label?: string; name?: string }> } } | undefined)?.data?.statuses) ?? []).map((s) => typeof s === "string" ? { value: s, label: s } : { value: s.value ?? s.id ?? s.name ?? "", label: s.label ?? s.name ?? s.value ?? s.id ?? "" });
76
+ const previewKey = `${connectionId}|${mode}|${qualified}|${unqualified}`;
77
+ const [previewed, setPreviewed] = useState("");
78
+ const preview = useG8Query((signal) => g8Get("/integrations/crm-sync/leads/push-preview", { connection_id: connectionId, source_mode: mode, status_qualified: qualified || undefined, status_unqualified: unqualified || undefined }, { signal }), [previewKey], { enabled: false });
79
+
80
+ const n = Number(limit);
81
+ const limitOk = limit.trim() === "" || (Number.isInteger(n) && n > 0);
82
+ const cap = limit.trim() ? { limit: n } : {};
83
+ const scope = limit.trim() ? `up to ${n.toLocaleString()}` : "all";
84
+ const run = useG8Mutation(async (p: Plan) => {
85
+ const res = await g8Post<{ job_id?: string | null }>(p.path, p.body);
86
+ return { id: res.data?.job_id ?? null, label: p.label };
87
+ }, { onSuccess: (r) => { setPlan(null); if (r.id) setJobs((j) => [{ id: r.id as string, label: r.label }, ...j]); }, onError: () => setPlan(null) });
88
+
89
+ const plans = (kind: "contacts" | "deals"): Plan[] => [
90
+ { path: `/integrations/crm-sync/${kind}/push`, label: `Send ${kind} to ${label}`, scope: `${scope} ${brand.name} ${kind}, created or updated in ${label}`, body: { connection_id: connectionId, ...cap } },
91
+ { path: `/integrations/crm-sync/${kind}/pull`, label: `Bring ${kind} in from ${label}`, scope: `${scope} ${label} ${kind}, created or updated in ${brand.name}`, body: { connection_id: connectionId, ...cap } },
92
+ ];
93
+ const leadPush: Plan = { path: "/integrations/crm-sync/leads/push", label: `Send leads to ${label}`, scope: `${mode === "all_qualified" ? "qualified" : "all"} contacts as leads in ${label}`,
94
+ body: { connection_id: connectionId, source_mode: mode, ...(qualified ? { status_qualified: qualified } : {}), ...(unqualified ? { status_unqualified: unqualified } : {}), ...(bigOk ? { large_population_confirmed: true } : {}), ...cap } };
95
+ const leadPull: Plan = { path: "/integrations/crm-sync/leads/pull", label: `Bring leads in from ${label}`, scope: `${scope} ${label} leads (the saved lead pull settings), created or updated in ${brand.name}`, body: { connection_id: connectionId, ...cap } };
96
+ const pv = ((preview.data as { data?: Record<string, unknown> } | undefined)?.data ?? {}) as Record<string, unknown>;
97
+ const shown = previewed === previewKey && preview.data !== undefined;
98
+ const row = (p: Plan) => <Button key={p.path} size="sm" variant="outline" disabled={!limitOk} onClick={() => setPlan(p)}>{p.label}</Button>;
99
+ return (
100
+ <div className={`flex flex-col gap-4 ${className ?? ""}`}>
101
+ <div className="flex flex-col gap-1 sm:max-w-xs">
102
+ <Label htmlFor="sync-limit">Only the first N records (optional)</Label>
103
+ <Input id="sync-limit" inputMode="numeric" placeholder="All" value={limit} onChange={(e) => setLimit(e.target.value)} />
104
+ {!limitOk ? <p role="alert" className="text-xs text-error-foreground">Enter a whole number above zero, or leave it empty.</p> : null}
105
+ </div>
106
+ <SectionCard title="Contacts" description={`Between ${brand.name} and ${label}`}><div className="flex flex-wrap gap-2">{plans("contacts").map(row)}</div></SectionCard>
107
+ <SectionCard title="Deals" description={`Between ${brand.name} and ${label}`}><div className="flex flex-wrap gap-2">{plans("deals").map(row)}</div></SectionCard>
108
+ {provider === "salesforce" ? <SectionCard title="Leads" description="Check what would be sent first, then send">
109
+ <div className="flex flex-col gap-3">
110
+ <div className="grid gap-3 sm:grid-cols-3">
111
+ <div className="flex flex-col gap-1"><Label htmlFor="lead-mode">Send</Label><select id="lead-mode" className="h-9 rounded-md border bg-background px-2 text-sm" value={mode} onChange={(e) => setMode(e.target.value as typeof mode)}><option value="all_qualified">Qualified contacts</option><option value="all_contacts">All contacts</option></select></div>
112
+ <div className="flex flex-col gap-1"><Label htmlFor="lead-q">Status for qualified</Label><select id="lead-q" className="h-9 rounded-md border bg-background px-2 text-sm" value={qualified} onChange={(e) => setQualified(e.target.value)}><option value="">CRM default</option>{statusRows.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select></div>
113
+ <div className="flex flex-col gap-1"><Label htmlFor="lead-u">Status for others</Label><select id="lead-u" className="h-9 rounded-md border bg-background px-2 text-sm" value={unqualified} onChange={(e) => setUnqualified(e.target.value)}><option value="">CRM default</option>{statusRows.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select></div>
114
+ </div>
115
+ {statuses.error ? <ApiError compact error={statuses.error} title="Could not load the lead statuses" onRetry={() => void statuses.refetch()} /> : null}
116
+ <div className="flex flex-wrap items-center gap-2">
117
+ <Button size="sm" variant="outline" loading={preview.loading} onClick={() => { setPreviewed(previewKey); void preview.refetch(); }}>Preview the push</Button>
118
+ {shown ? <span role="status" className="text-sm" data-testid="lead-preview">{Object.keys(pv).length ? Object.entries(pv).filter(([, v]) => typeof v === "number" || typeof v === "string").slice(0, 4).map(([k, v]) => `${k.replace(/_/g, " ")}: ${String(v)}`).join(", ") : "Nothing would be sent."}</span> : null}
119
+ </div>
120
+ {preview.error && previewed === previewKey ? <ApiError compact error={preview.error} title="Could not preview the push" /> : null}
121
+ <label className="flex items-start gap-2 text-sm"><input type="checkbox" className="mt-1" checked={bigOk} onChange={(e) => setBigOk(e.target.checked)} /><span>I have checked the previewed count and accept a large push (needed only above the app's population limit).</span></label>
122
+ <div className="flex flex-wrap gap-2">{row(leadPush)}{row(leadPull)}</div>
123
+ </div>
124
+ </SectionCard> : null}
125
+ {run.error ? <ApiError error={run.error} title="The sync did not start" /> : null}
126
+ <SectionCard title={`Jobs (${jobs.length})`} description="Started in this session" flush>
127
+ {run.loading ? <Skeleton className="m-3 h-8" /> : null}
128
+ {jobs.length === 0 ? <p className="px-4 py-3 text-sm text-muted-foreground">No sync started yet. Pick one above.</p> : <ul className="divide-y">{jobs.map((j) => <JobRow key={j.id} job={j} />)}</ul>}
129
+ </SectionCard>
130
+ <ConfirmSpendDialog open={plan != null} tier="external" title={plan?.label ?? "Run a sync"} description={`This changes real data: ${plan?.scope ?? ""}. It runs in the background and cannot be recalled.`}
131
+ confirmLabel="Start sync" confirming={run.loading} onConfirm={() => plan && void run.mutate(plan)} onCancel={() => setPlan(null)} />
132
+ </div>
133
+ );
134
+ }