@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,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
+ }
@@ -26,6 +26,7 @@ import { ApiError } from "@/g8/components/api-error";
26
26
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
27
  import { formatWhen, PROVIDER_LABELS, TERMINAL_JOB_STATES } from "../lib/integrations-utils";
28
28
  import type { CrmProvider, CrmStatus } from "../lib/integrations-utils";
29
+ import { useBrand } from "@/g8/hooks/use-brand";
29
30
 
30
31
  export interface SyncStatusProps {
31
32
  provider: CrmProvider;
@@ -50,7 +51,7 @@ function JobProgress({ jobId }: { jobId: string }) {
50
51
  },
51
52
  );
52
53
  return (
53
- <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">
54
55
  Job {jobId}: {job.status?.status ?? "starting"}
55
56
  {job.polling ? "..." : ""}
56
57
  {job.error ? " (could not read the job status)" : ""}
@@ -59,6 +60,7 @@ function JobProgress({ jobId }: { jobId: string }) {
59
60
  }
60
61
 
61
62
  export function SyncStatus({ provider, records, className }: SyncStatusProps) {
63
+ const brand = useBrand();
62
64
  const [pending, setPending] = useState<Pending | null>(null);
63
65
  const [jobId, setJobId] = useState<string | null>(null);
64
66
  const [pushed, setPushed] = useState<string | null>(null);
@@ -115,8 +117,8 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
115
117
  const s = status.data?.data;
116
118
  if (!s?.connected) {
117
119
  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 role="status" className={`rounded border border-warning-foreground/25 bg-warning p-3 text-sm text-warning-foreground ${className ?? ""}`}>
121
+ {label} is not connected. {s?.message ?? `Connect it in ${brand.linkLabel}, then come back.`}
120
122
  </p>
121
123
  );
122
124
  }
@@ -132,7 +134,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
132
134
  ? {
133
135
  title: `${pending.enabled ? "Turn on" : "Turn off"} continuous ${pending.direction} with ${label}`,
134
136
  description: pending.enabled
135
- ? `graph8 will keep ${pending.direction === "push" ? "writing contacts to" : "reading contacts from"} your CRM.`
137
+ ? `${brand.linkLabel} will keep ${pending.direction === "push" ? "writing contacts to" : "reading contacts from"} your CRM.`
136
138
  : "Syncing in this direction stops.",
137
139
  label: pending.enabled ? "Turn on" : "Turn off",
138
140
  }
@@ -143,7 +145,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
143
145
  <div className="flex items-center justify-between">
144
146
  <div>
145
147
  <h3 className="font-medium">{label}</h3>
146
- <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>
147
149
  </div>
148
150
  <Button size="sm" variant="outline" onClick={() => void status.refetch()}>
149
151
  Recheck
@@ -151,8 +153,8 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
151
153
  </div>
152
154
 
153
155
  {(["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
+ <div key={direction} className="flex items-center justify-between rounded border border-border px-3 py-2 text-sm">
157
+ <span>{direction === "push" ? `Send ${brand.linkLabel} contacts to the CRM` : `Bring CRM contacts into ${brand.linkLabel}`}</span>
156
158
  <span className="flex gap-2">
157
159
  <Button
158
160
  size="sm"
@@ -179,7 +181,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
179
181
  <Button onClick={() => setPending({ kind: "push" })}>Push {records.length} contacts now</Button>
180
182
  ) : null}
181
183
  {pushed ? (
182
- <p role="status" className="text-sm text-green-700">
184
+ <p role="status" className="text-sm text-positive-foreground">
183
185
  {pushed}
184
186
  </p>
185
187
  ) : null}
@@ -197,7 +199,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
197
199
  ) : (
198
200
  <ul className="mt-1 flex flex-wrap gap-1 text-xs">
199
201
  {(fields.data ?? []).map((f, i) => (
200
- <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">
201
203
  {f.label ?? f.name}
202
204
  </li>
203
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
+ }
@@ -0,0 +1,179 @@
1
+ "use client";
2
+
3
+ /**
4
+ * WebhookDetail: the complete screen for one webhook. Finds it by id (so `?id=` deep links work), shows every field (endpoint, name,
5
+ * events, active state, field filters, created and updated times), its delivery log, and every action: rename, change the endpoint or
6
+ * the events, pause or resume, rotate the signing secret (shown once), delete.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. listWebhooks GET /webhooks (read)
10
+ * getWebhook GET /webhooks/{id} (read; only when the id is not in the list, e.g. a deep link to a webhook beyond the list's page)
11
+ * 2. listWebhookEvents GET /webhooks/events (read; the event names to pick from)
12
+ * 3. listWebhookDeliveries GET /webhooks/{id}/deliveries (read)
13
+ * 4. updateWebhook PATCH /webhooks/{id} (external, confirmed; edit, pause, resume)
14
+ * 5. rotateWebhookSecret POST /webhooks/{id}/rotate-secret (external, confirmed; new secret shown once)
15
+ * 6. deleteWebhook DELETE /webhooks/{id} (destructive, confirmed; 404 counts as done)
16
+ * Verify deliveries on the server with g8.webhooks.constructEvent; the secret never belongs in browser code.
17
+ */
18
+ import { useEffect, useState, type ReactNode } from "react";
19
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
22
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
23
+ import { LoadMore } from "@/g8/components/paging-notices";
24
+ import { EmptyState } from "@/g8/components/empty-state";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
27
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
28
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
29
+ import { SectionCard } from "@/g8/components/screen/section-card";
30
+ import { StatusPill } from "@/g8/components/screen/status-pill";
31
+ import { arrayOf, formatWhen } from "../lib/integrations-utils";
32
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
33
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
34
+ import { useBrand } from "@/g8/hooks/use-brand";
35
+
36
+ export interface WebhookDetailProps {
37
+ webhookId: string;
38
+ onClose?: () => void;
39
+ onExpand?: () => void;
40
+ onCollapse?: () => void;
41
+ onChanged?: () => void;
42
+ /** Extra header actions from the screen's `renderDetailActions` slot. */
43
+ extraActions?: ReactNode;
44
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
45
+ extraTabs?: { id: string; label: string; content: ReactNode }[];
46
+ }
47
+
48
+ interface Hook { id: string; url: string; name?: string | null; events?: string[]; is_active?: boolean; created_at?: string | null; updated_at?: string | null; field_filters?: Record<string, string[]> | null; secret?: string | null }
49
+ interface EventType { event: string; category?: string | null; description?: string | null }
50
+ interface Delivery { id: string; event: string; status: string; attempts?: number; max_attempts?: number; response_code?: number | null; error_message?: string | null; created_at?: string | null; completed_at?: string | null }
51
+
52
+ /** Deliveries per read; "Load more" appends the next page. */
53
+ const DELIVERY_PAGE = 20;
54
+
55
+ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WebhookDetailProps) {
56
+ const brand = useBrand();
57
+ const { ask, dialog, failure } = useConfirmAction();
58
+ const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
59
+ const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
60
+ const deliveries = useLoadMoreList(byPage<Delivery>(DELIVERY_PAGE, async (page, signal) => {
61
+ const env = await g8Get(`/webhooks/${webhookId}/deliveries`, { page, limit: DELIVERY_PAGE }, { signal });
62
+ const rows = arrayOf<Delivery>(env);
63
+ return { rows, hasNext: hasNextOf(env, DELIVERY_PAGE, rows.length) };
64
+ }), [webhookId]);
65
+ const listed = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
66
+ const single = useG8Query((signal) => g8Get<Hook>(`/webhooks/${webhookId}`, undefined, { signal }), [webhookId], { enabled: Boolean(hooks.data) && !listed });
67
+ const fetched = (single.data as { data?: Hook } | undefined)?.data;
68
+ // Only the record fetched for THIS id counts: useG8Query keeps the previous webhook's data while the next one loads.
69
+ const hook = listed ?? (fetched?.id === webhookId ? fetched : undefined);
70
+ const [form, setForm] = useState({ url: "", name: "", events: [] as string[] });
71
+ const [secret, setSecret] = useState<string | null>(null);
72
+
73
+ useEffect(() => {
74
+ if (hook) setForm({ url: hook.url, name: hook.name ?? "", events: hook.events ?? [] });
75
+ }, [hook?.id, hook?.url, hook?.name, hook?.events?.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
76
+
77
+ if (hooks.error) return <ApiError error={hooks.error} title="Could not load the webhook" onRetry={() => void hooks.refetch()} />;
78
+ if (!hook && single.error) return <ApiError error={single.error} title="Could not load the webhook" onRetry={() => void single.refetch()} />;
79
+ if (!hooks.data || (!hook && single.loading)) return <Skeleton className="h-40 w-full" />;
80
+ if (!hook) return <EmptyState title="Webhook not found" description="It may have been deleted." />;
81
+ const changed = () => { void hooks.refetch(); if (!listed) void single.refetch(); onChanged?.(); };
82
+ const active = hook.is_active !== false;
83
+ const byCategory = arrayOf<EventType>(events.data).reduce<Record<string, EventType[]>>((acc, e) => ({ ...acc, [e.category ?? "other"]: [...(acc[e.category ?? "other"] ?? []), e] }), {});
84
+ const toggle = (event: string) => setForm((f) => ({ ...f, events: f.events.includes(event) ? f.events.filter((x) => x !== event) : [...f.events, event] }));
85
+ const urlOk = /^https:\/\/\S+$/.test(form.url.trim());
86
+ const rows = deliveries.data ?? [];
87
+
88
+ const actions = (
89
+ <div className="flex flex-wrap gap-1">
90
+ <AskButton ask={ask} label={active ? "Pause" : "Resume"} tier="external" title={`${active ? "Pause" : "Resume"} deliveries to ${hook.url}`} confirmLabel={active ? "Pause" : "Resume"}
91
+ description={active ? "Events stop being sent to this endpoint until you resume." : "Events are sent to this endpoint again."} run={() => g8Patch(`/webhooks/${hook.id}`, { is_active: !active })} onDone={changed} />
92
+ <AskButton ask={ask} label="Rotate secret" tier="external" title={`Rotate the signing secret for ${hook.url}`} confirmLabel="Rotate"
93
+ description="The old secret stops verifying deliveries as soon as you rotate. Update your server before or right after."
94
+ run={async () => { const res = await g8Post<Hook>(`/webhooks/${hook.id}/rotate-secret`); if (res.data?.secret) setSecret(res.data.secret); }} onDone={changed} />
95
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete the webhook for ${hook.url}`} confirmLabel="Delete" tolerateStatus={[404]}
96
+ description={`${brand.linkLabel} stops sending events to this endpoint and removes its delivery log.`} run={() => g8Delete(`/webhooks/${hook.id}`)} onDone={() => { changed(); onClose?.(); }} />
97
+ </div>
98
+ );
99
+
100
+ const tabs = [
101
+ {
102
+ id: "overview",
103
+ label: "Overview",
104
+ content: (
105
+ <div className="space-y-4">
106
+ {secret ? (
107
+ <div role="alert" className="rounded border border-warning-foreground/30 bg-warning p-3 text-sm">
108
+ <p className="font-medium">New signing secret. It is shown once.</p>
109
+ <code className="mt-1 block break-all rounded bg-background p-2">{secret}</code>
110
+ <p className="mt-1 text-xs">Store it as G8_WEBHOOK_SECRET on your server. Never put it in browser code.</p>
111
+ <Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>I saved it</Button>
112
+ </div>
113
+ ) : null}
114
+ <KeyValueList columns={2} items={[
115
+ { label: "Endpoint", value: hook.url },
116
+ { label: "Name", value: hook.name ?? "-" },
117
+ { label: "Status", value: active ? "active" : "paused" },
118
+ { label: "Created", value: formatWhen(hook.created_at) },
119
+ { label: "Updated", value: formatWhen(hook.updated_at) },
120
+ { label: "Events", value: (hook.events ?? []).length },
121
+ ]} />
122
+ <SectionCard title="Subscribed events"><ul className="flex flex-wrap gap-1 text-xs">{(hook.events ?? []).map((e) => <li key={e}><StatusPill tone="neutral">{e}</StatusPill></li>)}</ul></SectionCard>
123
+ {hook.field_filters && Object.keys(hook.field_filters).length > 0 ? (
124
+ <SectionCard title="Field filters" description="crm.record.updated fires only when a watched field changed."><pre className="overflow-x-auto text-xs">{JSON.stringify(hook.field_filters, null, 2)}</pre></SectionCard>
125
+ ) : null}
126
+ </div>
127
+ ),
128
+ },
129
+ {
130
+ id: "edit",
131
+ label: "Edit",
132
+ content: (
133
+ <SectionCard title="Edit webhook" description="Changing where events go is confirmed first.">
134
+ <div className="space-y-3">
135
+ <div className="flex flex-col gap-1"><Label htmlFor="wd-url">Endpoint URL</Label><Input id="wd-url" value={form.url} onChange={(e) => setForm({ ...form, url: e.target.value })} /></div>
136
+ {form.url && !urlOk ? <p className="text-xs text-error-foreground">The endpoint must be an https:// URL.</p> : null}
137
+ <div className="flex flex-col gap-1"><Label htmlFor="wd-name">Name</Label><Input id="wd-name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>
138
+ {Object.entries(byCategory).map(([category, items]) => (
139
+ <fieldset key={category}>
140
+ <legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
141
+ <div className="flex flex-wrap gap-x-4 gap-y-1">
142
+ {items.map((e) => <label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}><input type="checkbox" checked={form.events.includes(e.event)} onChange={() => toggle(e.event)} />{e.event}</label>)}
143
+ </div>
144
+ </fieldset>
145
+ ))}
146
+ <AskButton ask={ask} label="Save changes" variant="default" tier="external" title={`Update the webhook for ${hook.url}`} confirmLabel="Save" disabled={!urlOk || form.events.length === 0}
147
+ description={`Changes where ${brand.linkLabel} sends your data.`} run={() => g8Patch(`/webhooks/${hook.id}`, { url: form.url.trim(), name: form.name.trim() || null, events: form.events })} onDone={changed} />
148
+ </div>
149
+ </SectionCard>
150
+ ),
151
+ },
152
+ {
153
+ id: "deliveries",
154
+ label: "Deliveries",
155
+ content: deliveries.loading && !deliveries.data ? <Skeleton className="h-16 w-full" /> : deliveries.error ? <ApiError error={deliveries.error} onRetry={() => void deliveries.refetch()} /> : rows.length === 0 ? <p className="text-sm text-muted-foreground">No deliveries yet.</p> : (
156
+ <>
157
+ <table className="w-full text-sm" aria-label="Deliveries">
158
+ <thead><tr className="text-left text-muted-foreground"><th>Event</th><th>Status</th><th>Code</th><th>Attempts</th><th>When</th></tr></thead>
159
+ <tbody>
160
+ {rows.map((d) => (
161
+ <tr key={d.id} className="border-t" title={d.error_message ?? undefined}>
162
+ <td>{d.event}</td><td>{d.status}</td><td>{d.response_code ?? "-"}</td><td>{d.attempts ?? 1}{d.max_attempts ? ` of ${d.max_attempts}` : ""}</td><td>{formatWhen(d.created_at)}</td>
163
+ </tr>
164
+ ))}
165
+ </tbody>
166
+ </table>
167
+ <LoadMore shown={rows.length} noun="deliveries" hasMore={deliveries.hasMore} loading={deliveries.loadingMore} onLoadMore={deliveries.loadMore} />
168
+ </>
169
+ ),
170
+ },
171
+ ];
172
+ return (
173
+ <>
174
+ <DetailPanel title={hook.name || hook.url} subtitle={hook.name ? hook.url : undefined} badges={<StatusPill tone={active ? "positive" : "neutral"}>{active ? "active" : "paused"}</StatusPill>} actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
175
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
176
+ <ConfirmSpendDialog {...dialog} />
177
+ </>
178
+ );
179
+ }