@graph8/sdk 0.361.0 → 0.370.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -85,6 +85,50 @@
85
85
  "external",
86
86
  "destructive"
87
87
  ]
88
+ },
89
+ {
90
+ "name": "IntegrationsScreen",
91
+ "file": "components/integrations-screen.tsx",
92
+ "purpose": "Finished integrations page: CRM and tool connections with a sync side panel, plus webhook management.",
93
+ "operations": [
94
+ "list_crm_syncs_crm_syncs_get",
95
+ "list_webhooks_webhooks_get"
96
+ ],
97
+ "tiers": [
98
+ "read"
99
+ ]
100
+ },
101
+ {
102
+ "name": "CrmDetail",
103
+ "file": "components/crm-detail.tsx",
104
+ "purpose": "Complete screen for one CRM integration: connection, sync switches, manual push, disconnect.",
105
+ "operations": [
106
+ "list_crm_syncs_crm_syncs_get",
107
+ "list_connections_integrations_connections_get",
108
+ "delete_connection_integrations_connections__connection_id__delete"
109
+ ],
110
+ "tiers": [
111
+ "read",
112
+ "destructive"
113
+ ]
114
+ },
115
+ {
116
+ "name": "WebhookDetail",
117
+ "file": "components/webhook-detail.tsx",
118
+ "purpose": "Complete screen for one webhook: every field, edit, deliveries, pause, rotate secret, delete.",
119
+ "operations": [
120
+ "list_webhooks_webhooks_get",
121
+ "list_webhook_events_webhooks_events_get",
122
+ "list_webhook_deliveries_webhooks__webhook_id__deliveries_get",
123
+ "update_webhook_webhooks__webhook_id__patch",
124
+ "rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post",
125
+ "delete_webhook_webhooks__webhook_id__delete"
126
+ ],
127
+ "tiers": [
128
+ "read",
129
+ "external",
130
+ "destructive"
131
+ ]
88
132
  }
89
133
  ],
90
134
  "hooks": [],
@@ -167,6 +211,7 @@
167
211
  }
168
212
  ],
169
213
  "peerDependencies": {
170
- "react": ">=18"
214
+ "react": ">=18",
215
+ "lucide-react": ">=0.400"
171
216
  }
172
217
  }
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CrmDetail: the complete screen for one CRM integration (HubSpot, Salesforce, Pipedrive, Zoho, SugarCRM). Shows its connection
5
+ * (status, connected and last sync time, connection id), the continuous sync switches, job progress, manual push and contact fields
6
+ * (SyncStatus), and the actions: disconnect (destructive) when connected, or a link to connect in graph8 when it is not.
7
+ * Connecting is a link out: the OAuth consent screen only runs in the graph8 app.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. listCrmSyncs GET /crm-syncs (read)
11
+ * 2. listConnections GET /integrations/connections (read; the connection to disconnect)
12
+ * 3. deleteConnection DELETE /integrations/connections/{id}?provider_config_key= (destructive, confirmed)
13
+ * 4. SyncStatus see sync-status.tsx (getCrmStatus, getCrmFields, set push/pull enabled, getSyncJob, pushContactsToCrm)
14
+ */
15
+ import type { ReactNode } from "react";
16
+ import { Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
20
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
21
+ import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { StatusPill } from "@/g8/components/screen/status-pill";
24
+ import { arrayOf, formatWhen, GRAPH8_APP_INTEGRATIONS_PATH, PROVIDER_LABELS } from "../lib/integrations-utils";
25
+ import type { ConnectionRow, CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
28
+ import { SyncStatus } from "./sync-status";
29
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
30
+
31
+ export interface CrmDetailProps {
32
+ provider: CrmProvider;
33
+ /** Where "Connect in graph8" sends the person. */
34
+ connectUrl?: string;
35
+ /** Contact records (in the CRM's field names) offered for a manual push. */
36
+ records?: ReadonlyArray<Record<string, unknown>>;
37
+ onClose?: () => void;
38
+ onExpand?: () => void;
39
+ onCollapse?: () => void;
40
+ onChanged?: () => void;
41
+ /** Extra header actions from the screen's `renderDetailActions` slot. */
42
+ extraActions?: ReactNode;
43
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
44
+ extraTabs?: { id: string; label: string; content: ReactNode }[];
45
+ }
46
+
47
+ const CONNECTED = ["connected", "active", "ok", "healthy", "syncing", "synced", "enabled", "linked"];
48
+
49
+ export function CrmDetail({ provider, connectUrl: connectUrlProp, records, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: CrmDetailProps) {
50
+ const brand = useBrand();
51
+ const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
52
+ const { ask, dialog, failure } = useConfirmAction();
53
+ const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
54
+ const connections = useG8Query((signal) => g8Get("/integrations/connections", undefined, { signal }), []);
55
+ const label = PROVIDER_LABELS[provider];
56
+
57
+ if (syncs.error) return <ApiError error={syncs.error} title="Could not load the integration" onRetry={() => void syncs.refetch()} />;
58
+ if (!syncs.data) return <Skeleton className="h-40 w-full" />;
59
+ const row = arrayOf<CrmSyncRow>(syncs.data).find((r) => r.provider?.toLowerCase() === provider);
60
+ const connected = Boolean(row && CONNECTED.includes(row.status?.toLowerCase()));
61
+ const all = arrayOf<ConnectionRow>(connections.data).filter((c) => (c.provider ?? "").toLowerCase() === provider || (c.provider_config_key ?? "").toLowerCase().split(/[-_]/)[0] === provider);
62
+ const connection = all.find((c) => row?.connection_id && c.connection_id === row.connection_id) ?? (all.length === 1 ? all[0] : undefined);
63
+ const changed = () => { void syncs.refetch(); void connections.refetch(); onChanged?.(); };
64
+
65
+ const actions = connected ? (
66
+ <AskButton ask={ask} label="Disconnect" variant="ghost" tier="destructive" title={`Disconnect ${label}`} confirmLabel="Disconnect"
67
+ disabled={!connection?.connection_id || !connection.provider_config_key}
68
+ description="Syncs configured against this connection stop working. Records already synced stay."
69
+ run={() => g8Delete(`/integrations/connections/${connection!.connection_id}`, { query: { provider_config_key: connection!.provider_config_key } })} onDone={changed} />
70
+ ) : (
71
+ <a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel}</a>
72
+ );
73
+
74
+ const tabs = [
75
+ {
76
+ id: "overview",
77
+ label: "Overview",
78
+ content: (
79
+ <KeyValueList columns={2} items={[
80
+ { label: "Provider", value: label },
81
+ { label: "Status", value: row?.status ?? "not connected" },
82
+ { label: "Connected", value: formatWhen(row?.connected_at) },
83
+ { label: "Last sync", value: formatWhen(row?.last_sync_at) },
84
+ { label: "Connection", value: row?.connection_id ?? connection?.connection_id ?? "-" },
85
+ { label: "Config key", value: connection?.provider_config_key ?? "-" },
86
+ ]} />
87
+ ),
88
+ },
89
+ { id: "sync", label: "Sync", content: <SyncStatus provider={provider} records={records} /> },
90
+ ];
91
+ return (
92
+ <>
93
+ <DetailPanel title={label} subtitle="CRM integration" avatar={<EntityAvatar name={label} kind="company" size="lg" />}
94
+ badges={<StatusPill tone={connected ? "positive" : "neutral"}>{connected ? "Connected" : row?.status ?? "Not connected"}</StatusPill>}
95
+ actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
96
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
97
+ <ConfirmSpendDialog {...dialog} />
98
+ </>
99
+ );
100
+ }
@@ -26,17 +26,20 @@ import {
26
26
  arrayOf,
27
27
  CRM_PROVIDERS,
28
28
  formatWhen,
29
- GRAPH8_APP_INTEGRATIONS_URL,
29
+ GRAPH8_APP_INTEGRATIONS_PATH,
30
30
  looksConnected,
31
31
  PROVIDER_LABELS,
32
32
  } from "../lib/integrations-utils";
33
33
  import type { ConnectionRow, CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
34
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
34
35
 
35
36
  export interface IntegrationListProps {
36
37
  /** Where "Connect" sends the person. Default: graph8 Settings > Integrations. */
37
38
  connectUrl?: string;
38
39
  /** Called when the person opens a CRM's sync panel (render SyncStatus for it). */
39
40
  onOpenSync?: (provider: CrmProvider) => void;
41
+ /** Called when a person opens any CRM, connected or not (show a CrmDetail). */
42
+ onSelectCrm?: (provider: CrmProvider) => void;
40
43
  className?: string;
41
44
  }
42
45
 
@@ -78,7 +81,9 @@ function ChatTools() {
78
81
  );
79
82
  }
80
83
 
81
- export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOpenSync, className }: IntegrationListProps) {
84
+ export function IntegrationList({ connectUrl: connectUrlProp, onOpenSync, onSelectCrm, className }: IntegrationListProps) {
85
+ const brand = useBrand();
86
+ const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
82
87
  const [disconnecting, setDisconnecting] = useState<{ provider: CrmProvider; connection: ConnectionRow } | null>(null);
83
88
  const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
84
89
  const connections = useG8Query((signal) => g8Get("/integrations/connections", undefined, { signal }), []);
@@ -113,7 +118,7 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
113
118
  return (
114
119
  <li key={p} className="flex flex-wrap items-center justify-between gap-2 px-3 py-3" data-provider={p}>
115
120
  <div>
116
- <div className="font-medium">{PROVIDER_LABELS[p]}</div>
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>}
117
122
  <div className="text-xs text-gray-500">
118
123
  {connected ? `Last sync: ${formatWhen(row?.last_sync_at)}` : "Connect to sync contacts and companies."}
119
124
  </div>
@@ -137,7 +142,7 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
137
142
  ) : (
138
143
  <Button size="sm" asChild>
139
144
  <a href={connectUrl} target="_blank" rel="noreferrer">
140
- Connect in graph8
145
+ Connect in {brand.linkLabel}
141
146
  </a>
142
147
  </Button>
143
148
  )}
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ /**
4
+ * IntegrationsScreen: the finished integrations settings page. Two tabs:
5
+ * Connections the five CRMs and the workspace tools with their status (IntegrationList). Every CRM, connected or not, opens its
6
+ * complete detail screen (CrmDetail: connection, push/pull switches, job progress, manual push, disconnect; `?id=hubspot`).
7
+ * "Connect in graph8" links out: the OAuth consent screen only runs in the graph8 app.
8
+ * Webhooks create webhooks (WebhookManager); each opens its complete detail screen (WebhookDetail: every field, edit, deliveries,
9
+ * pause, rotate secret, delete; `?id=`). Every detail also opens as a full page with `?view=full`.
10
+ *
11
+ * 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).
13
+ * Prerequisites: a CRM connected in the app (where: "app"); a public https route with G8_WEBHOOK_SECRET for webhooks.
14
+ */
15
+ import type { ReactNode } from "react";
16
+ import { useState } from "react";
17
+ import { Plug } from "lucide-react";
18
+ import { g8Get } from "@/g8/lib/g8-client";
19
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
20
+ import { useUrlState } from "@/g8/hooks/use-url-state";
21
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
22
+ import type { StatItem } from "@/g8/components/screen/stat-strip";
23
+ import { SplitView } from "@/g8/components/screen/split-view";
24
+ import { SectionCard } from "@/g8/components/screen/section-card";
25
+ import { arrayOf, CRM_PROVIDERS, GRAPH8_APP_INTEGRATIONS_PATH, PROVIDER_LABELS } from "../lib/integrations-utils";
26
+ import type { CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
27
+ import { IntegrationList } from "./integration-list";
28
+ import { CrmDetail } from "./crm-detail";
29
+ import { WebhookDetail } from "./webhook-detail";
30
+ import { WebhookManager } from "./webhook-manager";
31
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
32
+
33
+ export interface IntegrationsScreenProps {
34
+ /** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
35
+ connectUrl?: string;
36
+ /** Pre-fill the webhook endpoint URL (your /api/webhooks/graph8 route). */
37
+ defaultWebhookUrl?: string;
38
+ /** Contact records (in the CRM's field names) offered for a manual push in the sync panel. */
39
+ pushRecords?: ReadonlyArray<Record<string, unknown>>;
40
+ toolbarActions?: ReactNode;
41
+ /** Buttons added to the open record's header: a CRM connection or a webhook. */
42
+ renderDetailActions?: (record: IntegrationRecord) => ReactNode;
43
+ /** Tabs added after the built-in ones on the open record. */
44
+ extraDetailTabs?: { id: string; label: string; render: (record: IntegrationRecord) => ReactNode }[];
45
+ className?: string;
46
+ }
47
+
48
+ /** The record open in the detail panel, passed to the slot props. */
49
+ export type IntegrationRecord = { kind: "crm"; provider: CrmProvider } | { kind: "webhook"; id: string };
50
+
51
+ const CONNECTED = ["connected", "active", "ok", "healthy", "syncing", "synced", "enabled", "linked"];
52
+
53
+ function useStats(): StatItem[] | undefined {
54
+ const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
55
+ const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
56
+ if (syncs.error) return undefined;
57
+ const rows = arrayOf<CrmSyncRow>(syncs.data);
58
+ const connected = rows.filter((r) => CONNECTED.includes(r.status?.toLowerCase())).length;
59
+ const items: StatItem[] = [
60
+ { label: "CRMs connected", value: `${connected} of ${CRM_PROVIDERS.length}`, tone: connected ? "positive" : "neutral" },
61
+ ];
62
+ if (!hooks.error && hooks.data) items.push({ label: "Webhooks", value: arrayOf<unknown>(hooks.data).length });
63
+ return syncs.data ? items : undefined;
64
+ }
65
+
66
+ export function IntegrationsScreen(props: IntegrationsScreenProps) {
67
+ const brand = useBrand();
68
+ const connectUrl = props.connectUrl ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
69
+ const [tabValue, setTab] = useUrlState("tab", "connections");
70
+ const tab = tabValue === "webhooks" ? "webhooks" : "connections";
71
+ const [id, setId] = useUrlState("id", "");
72
+ const [view, setView] = useUrlState("view", "");
73
+ const expanded = view === "full" && Boolean(id);
74
+ const [tick, setTick] = useState(0);
75
+ const stats = useStats();
76
+ const close = () => {
77
+ setId("");
78
+ setView("");
79
+ };
80
+ const panel = {
81
+ onClose: close,
82
+ onExpand: expanded ? undefined : () => setView("full"),
83
+ onCollapse: expanded ? () => setView("") : undefined,
84
+ onChanged: () => setTick((t) => t + 1),
85
+ };
86
+ const provider = (CRM_PROVIDERS as readonly string[]).includes(id) ? (id as CrmProvider) : null;
87
+ const slots = (record: IntegrationRecord) => ({
88
+ extraActions: props.renderDetailActions?.(record),
89
+ extraTabs: (props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(record) })),
90
+ });
91
+ const split = (list: ReactNode, detail: ReactNode, open: boolean) => (
92
+ <SplitView detailOpen={open} onCloseDetail={close} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : "")} detailWidth="lg" list={list} detail={open ? detail : null} />
93
+ );
94
+
95
+ return (
96
+ <ScreenLayout
97
+ title="Integrations"
98
+ description={`Connect your CRM, keep it in sync, and send ${brand.linkLabel} events to your own systems.`}
99
+ icon={<Plug className="size-5" />}
100
+ actions={props.toolbarActions}
101
+ tabs={[
102
+ { id: "connections", label: "Connections" },
103
+ { id: "webhooks", label: "Webhooks" },
104
+ ]}
105
+ activeTab={tab}
106
+ onTabChange={(next) => {
107
+ setTab(next);
108
+ close();
109
+ }}
110
+ stats={stats}
111
+ className={props.className}
112
+ >
113
+ {tab === "connections" ? (
114
+ <div className="space-y-4">
115
+ <SectionCard title="Connecting an account" description={`Sign-in with your CRM happens in ${brand.linkLabel}. Connect there, then manage the sync here.`}>
116
+ <a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">
117
+ Connect in {brand.linkLabel}
118
+ </a>
119
+ </SectionCard>
120
+ {split(
121
+ <IntegrationList key={tick} connectUrl={connectUrl} onSelectCrm={(p) => setId(p)} onOpenSync={(p) => setId(p)} />,
122
+ provider ? <CrmDetail key={provider} provider={provider} connectUrl={connectUrl} records={props.pushRecords} {...panel} {...slots({ kind: "crm", provider })} /> : null,
123
+ provider != null,
124
+ )}
125
+ </div>
126
+ ) : (
127
+ split(
128
+ <WebhookManager key={tick} defaultUrl={props.defaultWebhookUrl} onSelectWebhook={(h) => setId(h.id)} />,
129
+ <WebhookDetail key={id} webhookId={id} {...panel} {...slots({ kind: "webhook", id })} />,
130
+ Boolean(id),
131
+ )
132
+ )}
133
+ </ScreenLayout>
134
+ );
135
+ }
@@ -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;
@@ -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);
@@ -116,7 +118,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
116
118
  if (!s?.connected) {
117
119
  return (
118
120
  <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."}
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
  }
@@ -152,7 +154,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
152
154
 
153
155
  {(["push", "pull"] as const).map((direction) => (
154
156
  <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>
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"
@@ -0,0 +1,160 @@
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
+ * 2. listWebhookEvents GET /webhooks/events (read; the event names to pick from)
11
+ * 3. listWebhookDeliveries GET /webhooks/{id}/deliveries (read)
12
+ * 4. updateWebhook PATCH /webhooks/{id} (external, confirmed; edit, pause, resume)
13
+ * 5. rotateWebhookSecret POST /webhooks/{id}/rotate-secret (external, confirmed; new secret shown once)
14
+ * 6. deleteWebhook DELETE /webhooks/{id} (destructive, confirmed; 404 counts as done)
15
+ * Verify deliveries on the server with g8.webhooks.constructEvent; the secret never belongs in browser code.
16
+ */
17
+ import { useEffect, useState, type ReactNode } from "react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
24
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
25
+ import { SectionCard } from "@/g8/components/screen/section-card";
26
+ import { StatusPill } from "@/g8/components/screen/status-pill";
27
+ import { arrayOf, formatWhen } from "../lib/integrations-utils";
28
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
+ import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
30
+ import { useBrand } from "@/g8/hooks/use-brand";
31
+
32
+ export interface WebhookDetailProps {
33
+ webhookId: string;
34
+ onClose?: () => void;
35
+ onExpand?: () => void;
36
+ onCollapse?: () => void;
37
+ onChanged?: () => void;
38
+ /** Extra header actions from the screen's `renderDetailActions` slot. */
39
+ extraActions?: ReactNode;
40
+ /** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
41
+ extraTabs?: { id: string; label: string; content: ReactNode }[];
42
+ }
43
+
44
+ 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 }
45
+ interface EventType { event: string; category?: string | null; description?: string | null }
46
+ 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 }
47
+
48
+ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WebhookDetailProps) {
49
+ const brand = useBrand();
50
+ const { ask, dialog, failure } = useConfirmAction();
51
+ const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
52
+ const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
53
+ const deliveries = useG8Query((signal) => g8Get(`/webhooks/${webhookId}/deliveries`, { page: 1, limit: 20 }, { signal }), [webhookId]);
54
+ const hook = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
55
+ const [form, setForm] = useState({ url: "", name: "", events: [] as string[] });
56
+ const [secret, setSecret] = useState<string | null>(null);
57
+
58
+ useEffect(() => {
59
+ if (hook) setForm({ url: hook.url, name: hook.name ?? "", events: hook.events ?? [] });
60
+ }, [hook?.id, hook?.url, hook?.name, hook?.events?.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
61
+
62
+ if (hooks.error) return <ApiError error={hooks.error} title="Could not load the webhook" onRetry={() => void hooks.refetch()} />;
63
+ if (!hooks.data) return <Skeleton className="h-40 w-full" />;
64
+ if (!hook) return <EmptyState title="Webhook not found" description="It may have been deleted." />;
65
+ const changed = () => { void hooks.refetch(); onChanged?.(); };
66
+ const active = hook.is_active !== false;
67
+ const byCategory = arrayOf<EventType>(events.data).reduce<Record<string, EventType[]>>((acc, e) => ({ ...acc, [e.category ?? "other"]: [...(acc[e.category ?? "other"] ?? []), e] }), {});
68
+ const toggle = (event: string) => setForm((f) => ({ ...f, events: f.events.includes(event) ? f.events.filter((x) => x !== event) : [...f.events, event] }));
69
+ const urlOk = /^https:\/\/\S+$/.test(form.url.trim());
70
+ const rows = arrayOf<Delivery>(deliveries.data);
71
+
72
+ const actions = (
73
+ <div className="flex flex-wrap gap-1">
74
+ <AskButton ask={ask} label={active ? "Pause" : "Resume"} tier="external" title={`${active ? "Pause" : "Resume"} deliveries to ${hook.url}`} confirmLabel={active ? "Pause" : "Resume"}
75
+ 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} />
76
+ <AskButton ask={ask} label="Rotate secret" tier="external" title={`Rotate the signing secret for ${hook.url}`} confirmLabel="Rotate"
77
+ description="The old secret stops verifying deliveries as soon as you rotate. Update your server before or right after."
78
+ run={async () => { const res = await g8Post<Hook>(`/webhooks/${hook.id}/rotate-secret`); if (res.data?.secret) setSecret(res.data.secret); }} onDone={changed} />
79
+ <AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete the webhook for ${hook.url}`} confirmLabel="Delete" tolerateStatus={[404]}
80
+ description={`${brand.linkLabel} stops sending events to this endpoint and removes its delivery log.`} run={() => g8Delete(`/webhooks/${hook.id}`)} onDone={() => { changed(); onClose?.(); }} />
81
+ </div>
82
+ );
83
+
84
+ const tabs = [
85
+ {
86
+ id: "overview",
87
+ label: "Overview",
88
+ content: (
89
+ <div className="space-y-4">
90
+ {secret ? (
91
+ <div role="alert" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm">
92
+ <p className="font-medium">New signing secret. It is shown once.</p>
93
+ <code className="mt-1 block break-all rounded bg-white p-2">{secret}</code>
94
+ <p className="mt-1 text-xs">Store it as G8_WEBHOOK_SECRET on your server. Never put it in browser code.</p>
95
+ <Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>I saved it</Button>
96
+ </div>
97
+ ) : null}
98
+ <KeyValueList columns={2} items={[
99
+ { label: "Endpoint", value: hook.url },
100
+ { label: "Name", value: hook.name ?? "-" },
101
+ { label: "Status", value: active ? "active" : "paused" },
102
+ { label: "Created", value: formatWhen(hook.created_at) },
103
+ { label: "Updated", value: formatWhen(hook.updated_at) },
104
+ { label: "Events", value: (hook.events ?? []).length },
105
+ ]} />
106
+ <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>
107
+ {hook.field_filters && Object.keys(hook.field_filters).length > 0 ? (
108
+ <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>
109
+ ) : null}
110
+ </div>
111
+ ),
112
+ },
113
+ {
114
+ id: "edit",
115
+ label: "Edit",
116
+ content: (
117
+ <SectionCard title="Edit webhook" description="Changing where events go is confirmed first.">
118
+ <div className="space-y-3">
119
+ <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>
120
+ {form.url && !urlOk ? <p className="text-xs text-error-foreground">The endpoint must be an https:// URL.</p> : null}
121
+ <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>
122
+ {Object.entries(byCategory).map(([category, items]) => (
123
+ <fieldset key={category}>
124
+ <legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
125
+ <div className="flex flex-wrap gap-x-4 gap-y-1">
126
+ {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>)}
127
+ </div>
128
+ </fieldset>
129
+ ))}
130
+ <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}
131
+ 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} />
132
+ </div>
133
+ </SectionCard>
134
+ ),
135
+ },
136
+ {
137
+ id: "deliveries",
138
+ label: "Deliveries",
139
+ 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> : (
140
+ <table className="w-full text-sm" aria-label="Deliveries">
141
+ <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>
142
+ <tbody>
143
+ {rows.map((d) => (
144
+ <tr key={d.id} className="border-t" title={d.error_message ?? undefined}>
145
+ <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>
146
+ </tr>
147
+ ))}
148
+ </tbody>
149
+ </table>
150
+ ),
151
+ },
152
+ ];
153
+ return (
154
+ <>
155
+ <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} />
156
+ {failure ? <ApiError error={failure} title="That did not work" /> : null}
157
+ <ConfirmSpendDialog {...dialog} />
158
+ </>
159
+ );
160
+ }
@@ -27,10 +27,14 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
27
  import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
28
28
  import { EmptyState } from "@/g8/components/empty-state";
29
29
  import { arrayOf, formatWhen } from "../lib/integrations-utils";
30
+ import { plural } from "@/g8/lib/plural";
31
+ import { DEFAULT_BRAND, useBrand } from "@/g8/hooks/use-brand";
30
32
 
31
33
  export interface WebhookManagerProps {
32
34
  /** Pre-fill the endpoint URL of the create form (your /api/webhooks/graph8 route). */
33
35
  defaultUrl?: string;
36
+ /** Called when a person opens a webhook (show a WebhookDetail). */
37
+ onSelectWebhook?: (webhook: { id: string; url: string; name?: string | null }) => void;
34
38
  className?: string;
35
39
  }
36
40
 
@@ -65,13 +69,13 @@ type Pending =
65
69
  | { kind: "rotate"; hook: Webhook }
66
70
  | { kind: "delete"; hook: Webhook };
67
71
 
68
- function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: string; label: string } {
72
+ function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier: ConfirmTier; title: string; description: string; label: string } {
69
73
  switch (p.kind) {
70
74
  case "create":
71
75
  return {
72
76
  tier: "external",
73
- title: `Send ${p.events.length} event type${p.events.length === 1 ? "" : "s"} to ${p.url}`,
74
- description: "graph8 will POST signed events to this URL as they happen.",
77
+ title: `Send ${plural(p.events.length, "event type")} to ${p.url}`,
78
+ description: `${platform} will POST signed events to this URL as they happen.`,
75
79
  label: "Create webhook",
76
80
  };
77
81
  case "toggle":
@@ -80,7 +84,7 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
80
84
  title: `${p.hook.is_active === false ? "Resume" : "Pause"} deliveries to ${p.hook.url}`,
81
85
  description:
82
86
  p.hook.is_active === false
83
- ? "graph8 starts sending events to this URL again."
87
+ ? `${platform} starts sending events to this URL again.`
84
88
  : "Events stop being sent to this URL until you resume.",
85
89
  label: p.hook.is_active === false ? "Resume" : "Pause",
86
90
  };
@@ -133,7 +137,8 @@ function Deliveries({ hook }: { hook: Webhook }) {
133
137
  );
134
138
  }
135
139
 
136
- export function WebhookManager({ defaultUrl = "", className }: WebhookManagerProps) {
140
+ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }: WebhookManagerProps) {
141
+ const brand = useBrand();
137
142
  const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
138
143
  const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
139
144
  const [url, setUrl] = useState(defaultUrl);
@@ -169,7 +174,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
169
174
  );
170
175
  const urlOk = /^https:\/\/\S+$/.test(url.trim());
171
176
  const toggle = (event: string) => setChosen((c) => (c.includes(event) ? c.filter((x) => x !== event) : [...c, event]));
172
- const confirm = pending ? copyFor(pending) : null;
177
+ const confirm = pending ? copyFor(pending, brand.linkLabel) : null;
173
178
 
174
179
  return (
175
180
  <div className={`space-y-4 ${className ?? ""}`}>
@@ -178,7 +183,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
178
183
  <div className="grid gap-2 md:grid-cols-2">
179
184
  <Input
180
185
  aria-label="Endpoint URL"
181
- placeholder="https://yourapp.com/api/webhooks/graph8"
186
+ placeholder={`https://yourapp.com/api/webhooks/${brand.linkLabel.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`}
182
187
  value={url}
183
188
  onChange={(e) => setUrl(e.target.value)}
184
189
  />
@@ -238,7 +243,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
238
243
  <li key={h.id} className="space-y-2 px-3 py-3">
239
244
  <div className="flex flex-wrap items-center justify-between gap-2">
240
245
  <div>
241
- <div className="font-medium">{h.name || h.url}</div>
246
+ {onSelectWebhook ? <button type="button" className="font-medium hover:underline" onClick={() => onSelectWebhook(h)}>{h.name || h.url}</button> : <div className="font-medium">{h.name || h.url}</div>}
242
247
  <div className="text-xs text-gray-500">
243
248
  {h.name ? `${h.url} - ` : ""}
244
249
  {(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}