@graph8/sdk 0.370.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 (466) hide show
  1. package/README.md +19 -0
  2. package/blocks/_foundation/README.md +14 -0
  3. package/blocks/_foundation/block.json +44 -0
  4. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  5. package/blocks/_foundation/components/empty-state.tsx +5 -2
  6. package/blocks/_foundation/components/list-select.tsx +112 -0
  7. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  8. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  9. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  10. package/blocks/_foundation/components/screen/index.ts +1 -0
  11. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  12. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  13. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  14. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  15. package/blocks/_foundation/lib/paging.ts +57 -0
  16. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  17. package/blocks/agents/README.md +81 -4
  18. package/blocks/agents/block.json +311 -2
  19. package/blocks/agents/components/agent-activity.tsx +112 -0
  20. package/blocks/agents/components/agent-approvals.tsx +135 -0
  21. package/blocks/agents/components/agent-builder.tsx +17 -3
  22. package/blocks/agents/components/agent-conversations.tsx +143 -0
  23. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  24. package/blocks/agents/components/agent-operators.tsx +164 -0
  25. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  26. package/blocks/agents/components/agent-runs.tsx +3 -3
  27. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  28. package/blocks/agents/components/agent-test-history.tsx +121 -0
  29. package/blocks/agents/components/agents-screen.tsx +53 -11
  30. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  31. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  32. package/blocks/agents/components/from-template-button.tsx +65 -0
  33. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  34. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  35. package/blocks/agents/components/knowledge-form.tsx +69 -0
  36. package/blocks/agents/components/name-dialog.tsx +32 -0
  37. package/blocks/agents/components/receptionist-button.tsx +46 -0
  38. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  39. package/blocks/agents/components/skill-editor.tsx +147 -0
  40. package/blocks/agents/components/skill-library.tsx +111 -0
  41. package/blocks/agents/lib/agent-types.ts +7 -1
  42. package/blocks/agents/lib/facts.tsx +13 -0
  43. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  44. package/blocks/agents/lib/load-more.tsx +141 -0
  45. package/blocks/agents/lib/skill-utils.ts +91 -0
  46. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  47. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  48. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  49. package/blocks/analytics/components/report-builder.tsx +15 -2
  50. package/blocks/appointments/README.md +6 -0
  51. package/blocks/appointments/block.json +27 -0
  52. package/blocks/appointments/components/booking-page.tsx +2 -2
  53. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  54. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  55. package/blocks/appointments/components/meeting-list.tsx +29 -13
  56. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  57. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  58. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  59. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  60. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  61. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  62. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  63. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  64. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  65. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  66. package/blocks/coach/components/call-scorecard.tsx +7 -7
  67. package/blocks/company-intelligence/README.md +132 -0
  68. package/blocks/company-intelligence/block.json +318 -0
  69. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  70. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  71. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  72. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  73. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  74. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  75. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  76. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  77. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  78. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  79. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  80. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  81. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  82. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  83. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  84. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  85. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  86. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  87. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  88. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  89. package/blocks/crm/README.md +6 -0
  90. package/blocks/crm/block.json +53 -4
  91. package/blocks/crm/components/companies-screen.tsx +23 -9
  92. package/blocks/crm/components/company-detail.tsx +2 -2
  93. package/blocks/crm/components/contact-detail.tsx +4 -4
  94. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  95. package/blocks/crm/components/contacts-screen.tsx +31 -13
  96. package/blocks/crm/components/dedupe-review.tsx +6 -1
  97. package/blocks/crm/components/import-wizard.tsx +3 -7
  98. package/blocks/crm/components/list-select.tsx +20 -0
  99. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  100. package/blocks/crm/components/record-form.tsx +1 -1
  101. package/blocks/crm/components/tasks-screen.tsx +35 -15
  102. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  103. package/blocks/crm/hooks/use-page-query.ts +3 -1
  104. package/blocks/crm/lib/crm-types.ts +14 -0
  105. package/blocks/crm/lib/read-lists.ts +16 -0
  106. package/blocks/deals/README.md +24 -6
  107. package/blocks/deals/block.json +151 -2
  108. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  109. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  110. package/blocks/deals/components/deal-detail.tsx +2 -2
  111. package/blocks/deals/components/deal-filters.tsx +65 -0
  112. package/blocks/deals/components/deal-line-items.tsx +120 -0
  113. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  114. package/blocks/deals/components/deal-related.tsx +30 -9
  115. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  116. package/blocks/deals/components/deals-screen.tsx +57 -22
  117. package/blocks/deals/components/forecast-view.tsx +4 -4
  118. package/blocks/deals/components/pipeline-board.tsx +8 -6
  119. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  120. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  121. package/blocks/deals/components/stage-editor.tsx +1 -1
  122. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  123. package/blocks/deals/lib/deal-extras.ts +156 -0
  124. package/blocks/dialer/README.md +14 -5
  125. package/blocks/dialer/block.json +17 -2
  126. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  127. package/blocks/dialer/components/call-log-screen.tsx +4 -1
  128. package/blocks/dialer/components/call-log.tsx +16 -4
  129. package/blocks/dialer/components/call-queue.tsx +3 -3
  130. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  131. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  132. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  133. package/blocks/dialer/components/session-start-form.tsx +58 -11
  134. package/blocks/dialer/components/softphone.tsx +1 -1
  135. package/blocks/enrichment/README.md +16 -6
  136. package/blocks/enrichment/block.json +19 -7
  137. package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
  138. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  139. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  140. package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
  141. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  142. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  143. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  144. package/blocks/forms/README.md +8 -0
  145. package/blocks/forms/block.json +107 -0
  146. package/blocks/forms/components/form-activity.tsx +71 -0
  147. package/blocks/forms/components/form-backfill.tsx +108 -0
  148. package/blocks/forms/components/form-builder.tsx +1 -1
  149. package/blocks/forms/components/form-embed.tsx +1 -1
  150. package/blocks/forms/components/form-groups.tsx +151 -0
  151. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  152. package/blocks/forms/components/form-route-history.tsx +72 -0
  153. package/blocks/forms/components/form-saved-views.tsx +81 -0
  154. package/blocks/forms/components/forms-screen.tsx +51 -17
  155. package/blocks/forms/components/more-rows.tsx +30 -0
  156. package/blocks/forms/components/submissions-export.tsx +67 -0
  157. package/blocks/forms/components/submissions-table.tsx +15 -6
  158. package/blocks/forms/lib/forms-extra.ts +67 -0
  159. package/blocks/inbox/README.md +5 -0
  160. package/blocks/inbox/block.json +15 -0
  161. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  162. package/blocks/inbox/components/thread-badge.tsx +2 -2
  163. package/blocks/inbox/components/thread-view.tsx +1 -1
  164. package/blocks/integrations/README.md +54 -1
  165. package/blocks/integrations/block.json +189 -15
  166. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  167. package/blocks/integrations/components/connection-health.tsx +59 -0
  168. package/blocks/integrations/components/crm-detail.tsx +20 -1
  169. package/blocks/integrations/components/crm-records.tsx +103 -0
  170. package/blocks/integrations/components/field-mappings.tsx +61 -0
  171. package/blocks/integrations/components/integration-list.tsx +4 -4
  172. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  173. package/blocks/integrations/components/migration-panel.tsx +133 -0
  174. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  175. package/blocks/integrations/components/sync-status.tsx +6 -6
  176. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  177. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  178. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  179. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  180. package/blocks/integrations/lib/crm-utils.ts +39 -0
  181. package/blocks/integrations/lib/facts.tsx +13 -0
  182. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  183. package/blocks/integrations/lib/load-more.tsx +141 -0
  184. package/blocks/intent/components/account-intent-score.tsx +5 -5
  185. package/blocks/intent/components/intent-feed.tsx +8 -8
  186. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  187. package/blocks/intent/lib/intent-utils.ts +4 -4
  188. package/blocks/lifecycle/components/account-related.tsx +8 -1
  189. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  190. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  191. package/blocks/lists/README.md +7 -1
  192. package/blocks/lists/block.json +31 -3
  193. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  194. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  195. package/blocks/lists/components/list-picker.tsx +13 -3
  196. package/blocks/lists/components/list-table.tsx +1 -1
  197. package/blocks/lists/components/lists-screen.tsx +4 -2
  198. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  199. package/blocks/lists/components/suppression-manager.tsx +27 -6
  200. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  201. package/blocks/mailboxes/block.json +4 -0
  202. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  203. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  204. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  205. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  206. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  207. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  208. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  209. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  210. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  211. package/blocks/newsletter/README.md +12 -0
  212. package/blocks/newsletter/block.json +117 -0
  213. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  214. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  215. package/blocks/newsletter/components/more-rows.tsx +30 -0
  216. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  217. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  218. package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
  219. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  220. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  221. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  222. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  223. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  224. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  225. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  226. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  227. package/blocks/nurture/components/nurture-form.tsx +5 -8
  228. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  229. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  230. package/blocks/objects/README.md +1 -1
  231. package/blocks/objects/block.json +13 -5
  232. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  233. package/blocks/objects/components/object-record-detail.tsx +8 -1
  234. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  235. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  236. package/blocks/pages/README.md +14 -0
  237. package/blocks/pages/block.json +113 -1
  238. package/blocks/pages/components/more-rows.tsx +30 -0
  239. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  240. package/blocks/pages/components/page-analytics.tsx +176 -0
  241. package/blocks/pages/components/page-clone.tsx +99 -0
  242. package/blocks/pages/components/page-domains.tsx +167 -0
  243. package/blocks/pages/components/page-editor.tsx +1 -1
  244. package/blocks/pages/components/page-generation-status.tsx +48 -0
  245. package/blocks/pages/components/page-tracking.tsx +137 -0
  246. package/blocks/pages/components/pages-screen.tsx +57 -16
  247. package/blocks/pages/components/seo-report.tsx +6 -1
  248. package/blocks/pages/lib/studio.ts +7 -0
  249. package/blocks/phone-numbers/README.md +74 -0
  250. package/blocks/phone-numbers/block.json +262 -0
  251. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  252. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  253. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  254. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  255. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  256. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  257. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  258. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  259. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  260. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  261. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  262. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  263. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  264. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  265. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  266. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  267. package/blocks/pickers/README.md +23 -0
  268. package/blocks/pickers/block.json +103 -0
  269. package/blocks/pickers/components/company-picker.tsx +90 -0
  270. package/blocks/pickers/components/contact-picker.tsx +131 -0
  271. package/blocks/pickers/components/member-picker.tsx +99 -0
  272. package/blocks/pickers/components/search-picker.tsx +98 -0
  273. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  274. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  275. package/blocks/pickers/lib/use-debounced.ts +13 -0
  276. package/blocks/quotes/README.md +13 -0
  277. package/blocks/quotes/block.json +48 -1
  278. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  279. package/blocks/quotes/components/quote-builder.tsx +1 -1
  280. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  281. package/blocks/quotes/components/quote-list.tsx +5 -3
  282. package/blocks/quotes/components/quote-related.tsx +3 -2
  283. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  284. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  285. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  286. package/blocks/radar/components/radar-overview.tsx +4 -4
  287. package/blocks/radar/components/radar-screen.tsx +2 -2
  288. package/blocks/sequencer/README.md +6 -0
  289. package/blocks/sequencer/block.json +28 -1
  290. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  291. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  292. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  293. package/blocks/sequencer/components/sequence-builder.tsx +19 -20
  294. package/blocks/sequencer/components/sequence-detail.tsx +4 -2
  295. package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
  296. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  297. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  298. package/blocks/sequencer/components/sequences-screen.tsx +3 -0
  299. package/blocks/sequencer/components/step-editor.tsx +11 -11
  300. package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
  301. package/blocks/social-ads/README.md +8 -0
  302. package/blocks/social-ads/block.json +137 -4
  303. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  304. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  305. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  306. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  307. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  308. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  309. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  310. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  311. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  312. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  313. package/blocks/social-ads/components/post-composer.tsx +4 -2
  314. package/blocks/social-ads/components/post-detail.tsx +3 -0
  315. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  316. package/blocks/social-ads/components/social-performance.tsx +80 -0
  317. package/blocks/social-ads/components/social-screen.tsx +33 -6
  318. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  319. package/blocks/surveys/README.md +91 -0
  320. package/blocks/surveys/block.json +299 -0
  321. package/blocks/surveys/components/account-nps.tsx +69 -0
  322. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  323. package/blocks/surveys/components/org-summary.tsx +82 -0
  324. package/blocks/surveys/components/sending-domains.tsx +113 -0
  325. package/blocks/surveys/components/survey-form.tsx +126 -0
  326. package/blocks/surveys/components/survey-responses.tsx +145 -0
  327. package/blocks/surveys/components/survey-results.tsx +72 -0
  328. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  329. package/blocks/surveys/components/survey-send.tsx +162 -0
  330. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  331. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  332. package/blocks/surveys/lib/surveys-types.ts +228 -0
  333. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  334. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  335. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  336. package/blocks/visitors/components/snippet-install.tsx +18 -9
  337. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  338. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  339. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  340. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  341. package/blocks/webchat/components/conversation-list.tsx +1 -1
  342. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  343. package/blocks/workbench/components/run-progress.tsx +1 -1
  344. package/blocks/workbench/components/workbench-grid.tsx +1 -1
  345. package/blocks/workbench/components/workbench-screen.tsx +15 -5
  346. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  347. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  348. package/dist/cli.js +536 -392
  349. package/dist/index.browser.js +1 -1
  350. package/dist/index.browser.js.map +1 -1
  351. package/dist/index.browser.mjs +1 -1
  352. package/dist/index.browser.mjs.map +1 -1
  353. package/dist/index.d.mts +2 -2
  354. package/dist/index.d.ts +2 -2
  355. package/dist/index.js +1 -1
  356. package/dist/index.js.map +1 -1
  357. package/dist/index.mjs +1 -1
  358. package/dist/index.mjs.map +1 -1
  359. package/dist/react.browser.js +1 -1
  360. package/dist/react.browser.js.map +1 -1
  361. package/dist/react.browser.mjs +1 -1
  362. package/dist/react.browser.mjs.map +1 -1
  363. package/dist/react.d.mts +2 -2
  364. package/dist/react.d.ts +2 -2
  365. package/dist/react.js +1 -1
  366. package/dist/react.js.map +1 -1
  367. package/dist/react.mjs +1 -1
  368. package/dist/react.mjs.map +1 -1
  369. package/dist/types/blocks/brand.d.ts +11 -2
  370. package/dist/types/blocks/create.d.ts +7 -3
  371. package/dist/types/blocks/doctor.d.ts +1 -0
  372. package/dist/types/blocks/flows.d.ts +7 -1
  373. package/dist/types/blocks/orgBrand.d.ts +34 -0
  374. package/dist/types/cli.d.ts +3 -0
  375. package/dist/types/version.d.ts +2 -2
  376. package/docs/api/desk.d.ts +2 -2
  377. package/docs/api/desk.md +3 -3
  378. package/package.json +1 -1
  379. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  380. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  381. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  382. package/stacks/6sense.json +17 -4
  383. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  384. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  385. package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
  386. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  387. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  388. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  389. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  390. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  391. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  392. package/stacks/apollo.json +36 -8
  393. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  394. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  395. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  396. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  397. package/stacks/calendly.json +8 -4
  398. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  399. package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
  400. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  401. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  402. package/stacks/clay.json +16 -3
  403. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  404. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  405. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  406. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  407. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  408. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  409. package/stacks/gong.json +23 -6
  410. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  411. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  412. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  413. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  414. package/stacks/hootsuite.json +9 -4
  415. package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
  416. package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
  417. package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
  418. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  419. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  420. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  421. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  422. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  423. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  424. package/stacks/hubspot.json +21 -5
  425. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  426. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  427. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  428. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  429. package/stacks/intercom.json +8 -4
  430. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  431. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  432. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  433. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  434. package/stacks/mailchimp.json +20 -5
  435. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  436. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  437. package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
  438. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  439. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  440. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  441. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  442. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  443. package/stacks/outreach.json +25 -7
  444. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  445. package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
  446. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  447. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  448. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  449. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  450. package/stacks/pipedrive.json +4 -2
  451. package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
  452. package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
  453. package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
  454. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  455. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  456. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  457. package/stacks/salesforce.json +7 -3
  458. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  459. package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
  460. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  461. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  462. package/stacks/zoominfo.json +16 -3
  463. package/styles/styles.css +1 -1
  464. package/templates/next-app/app/globals.css.tmpl +349 -64
  465. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  466. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -22,6 +22,45 @@ export const TARGET_FIELDS = [
22
22
  ] as const;
23
23
  export type TargetField = (typeof TARGET_FIELDS)[number]["value"];
24
24
 
25
+ /**
26
+ * Providers the API can run for each field: the per-field waterfall templates the server builds pipelines from
27
+ * (a phone runs leadmagic `mobile_finder`, never an email finder). Any other provider is refused with 400
28
+ * `unsupported_provider_for_field`, so the UI only offers these.
29
+ */
30
+ export const FIELD_PROVIDERS: Record<TargetField, readonly string[]> = {
31
+ work_email: ["apollo", "leadmagic", "prospeo", "hunter", "icypeas", "dropcontact"],
32
+ direct_phone: ["leadmagic", "prospeo", "apollo"],
33
+ mobile_phone: ["leadmagic", "prospeo", "apollo"],
34
+ };
35
+
36
+ /** Providers a run tries for a field when nobody chose: the API's own defaults. */
37
+ export const FIELD_DEFAULT_PROVIDERS: Record<TargetField, readonly string[]> = {
38
+ work_email: ["prospeo", "dropcontact"],
39
+ direct_phone: ["leadmagic", "prospeo", "apollo"],
40
+ mobile_phone: ["leadmagic", "prospeo", "apollo"],
41
+ };
42
+
43
+ /**
44
+ * The providers to start from for `field`: `preferred` (in its order) kept to those the field supports, else the
45
+ * field's API defaults when none of `preferred` fits (for example email providers asked for a phone).
46
+ */
47
+ export function providersForField(field: TargetField, preferred?: readonly string[]): string[] {
48
+ const allowed = FIELD_PROVIDERS[field];
49
+ const kept = (preferred ?? FIELD_DEFAULT_PROVIDERS[field]).filter((p) => allowed.includes(p));
50
+ return kept.length ? [...new Set(kept)] : [...FIELD_DEFAULT_PROVIDERS[field]];
51
+ }
52
+
53
+ /** What POST /enrichment/waterfall/configs answers (created, or the list's existing pipeline for that field reused). */
54
+ export interface WaterfallPipelineResult {
55
+ column_id: string;
56
+ config_id?: string;
57
+ name?: string;
58
+ list_id?: number | null;
59
+ created: boolean;
60
+ providers?: string[];
61
+ missing_credentials?: string[];
62
+ }
63
+
25
64
  export interface ProviderEntry {
26
65
  provider: string;
27
66
  system_funded: boolean;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @title Enrich many contacts of a list and wait for the job
3
3
  * @when Filling work emails or phones for a list. Order: preflight (free: counts, missing provider keys and the
4
- * credit quote) -> a human approves the quote -> start the job -> poll it (the API has no streams or
4
+ * credit quote) -> a human approves the quote -> create or reuse the field's pipeline and run it -> poll it (the API has no streams or
5
5
  * webhook for completion) -> read provider errors if nothing was written. Contact ids must be on the list;
6
6
  * ids not on it are skipped and not charged. Never enrich contacts you only have as masked search results:
7
7
  * save them to a list first.
@@ -17,11 +17,22 @@ export async function quote(listId: number, contactIds: number[]) {
17
17
  return res.data; // contact_count, not_in_list, missing_credentials, warnings, credits_per_record, max_total_credits
18
18
  }
19
19
 
20
- /** Only after a person approved `max_total_credits`. */
20
+ /**
21
+ * Only after a person approved `max_total_credits`.
22
+ *
23
+ * Runs through a saved waterfall pipeline: create (or reuse) the list's work-email pipeline, then run it on the
24
+ * contacts. Creating is idempotent per (field, list): a list that already has a work-email pipeline reuses it with
25
+ * ITS providers (`created: false`). Keep the column_id: a retry only needs the second call.
26
+ */
21
27
  export async function enrichAndWait(listId: number, contactIds: number[]) {
22
- const started = await g8.api.enrichment.startEnrichment({
23
- body: { list_id: listId, contact_ids: contactIds, fields_config: { work_email: ["prospeo", "dropcontact"] } },
28
+ const pipeline = await g8.api.enrichment.createDefaultWaterfallConfig({
29
+ body: { list_id: listId, name: "Work email enrichment", field: "CONTACT_WORK_EMAIL", providers: ["prospeo", "dropcontact"] },
30
+ });
31
+ const started = await g8.api.enrichment.runWaterfallPipeline({
32
+ body: { column_id: pipeline.data.column_id, list_id: listId, record_ids: contactIds },
24
33
  });
34
+ // A 202 whose status is "error" never started (newer APIs answer 502): do not poll a job that will not exist.
35
+ if (!started.data.job_id || started.data.status === "error") throw new Error("enrichment did not start; nothing was charged");
25
36
  const jobId = started.data.job_id;
26
37
 
27
38
  let delay = 2000;
@@ -122,3 +122,11 @@ Tiers: `read` and `write` need no confirmation; `external` (pause, resume, retry
122
122
  | `forms.getSiteAudit` | `GET /api/v1/forms/site-audit` | read | `forms:read` | SiteAuditPanel |
123
123
  | `forms.requestSiteAuditScan` | `POST /api/v1/forms/site-audit/scan` | write | `forms:write` | SiteAuditPanel |
124
124
  | `forms.updateSiteAuditFinding` | `PATCH /api/v1/forms/site-audit/findings/{finding_id}` | write | `forms:write` | SiteAuditPanel |
125
+
126
+ ## Depth added (C7c)
127
+
128
+ graph8 captures forms from your site; there is **no create-form operation in the API** (an API gap, not a missing screen).
129
+
130
+ New on `FormsScreen`: a **Groups** tab (`FormGroups`: create, rename, re-type, add and remove forms, delete), saved views (`FormSavedViews`), "Load more" past the first 100 forms, and on an open form: a **Routing** tab (`FormBackfill`: preview, then a confirmed delivery; `FormRouteHistory`: restore the previous route or stop routing, both destructive confirms that send the version they read), an **Activity** tab (`FormActivity`: audit trail and pages), and **Export CSV** on Submissions (`SubmissionsExport`, formula-safe, says when the file is not the whole set).
131
+
132
+ Not covered (and why): `saveFormRoute` (the mapping editor's save already writes the route); `captureFormPreview` and `getFormPreview` return untyped image data; unidentified-form grouping (`previewUnidentifiedGrouping`, `setUnidentifiedGrouping`, `syncUnidentifiedGroup`) needs a list picker (owned by the pickers PR). The mapping editor's list select keeps its existing cap until that PR lands.
@@ -15,24 +15,37 @@
15
15
  "crm"
16
16
  ],
17
17
  "operations": [
18
+ "add_form_group_members_forms_groups__group_id__members_post",
19
+ "create_form_backfill_forms__form_key__backfills_post",
20
+ "create_form_group_forms_groups_post",
21
+ "delete_form_group_forms_groups__group_id__delete",
18
22
  "get_contact_form_fields_contacts_form_fields_get",
23
+ "get_form_activity_forms__form_key__activity_get",
24
+ "get_form_backfill_forms__form_key__backfills__job_id__get",
19
25
  "get_form_fields_forms__form_key__fields_get",
20
26
  "get_form_overview_forms__form_key__overview_get",
27
+ "get_form_placements_forms__form_key__placements_get",
21
28
  "get_form_route_recommendations_forms__form_key__route_recommendations_get",
22
29
  "get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
23
30
  "get_form_template_snippet_form_get",
24
31
  "get_org_snippet_snippet_get",
25
32
  "get_site_audit_forms_site_audit_get",
33
+ "list_form_groups_forms_groups_get",
26
34
  "list_forms_forms_get",
27
35
  "list_lists_lists_get",
36
+ "list_saved_form_views_forms_views_saved_get",
28
37
  "pause_form_delivery_forms__form_key__delivery_pause_post",
29
38
  "preview_form_mapping_forms__form_key__mapping_preview_post",
39
+ "remove_form_group_members_forms_groups__group_id__members_remove_post",
30
40
  "request_site_audit_scan_forms_site_audit_scan_post",
31
41
  "resume_form_delivery_forms__form_key__delivery_resume_post",
32
42
  "retry_form_delivery_forms_deliveries__receipt_id__retry_post",
43
+ "rollback_form_route_forms__form_key__route_delete",
33
44
  "save_form_mapping_forms__form_key__mapping_save_post",
45
+ "save_form_view_forms_views_saved__view_id__put",
34
46
  "search_form_submissions_forms__form_key__submissions_post",
35
47
  "test_form_route_forms__form_key__route_test_post",
48
+ "update_form_group_forms_groups__group_id__patch",
36
49
  "update_form_registry_forms__form_key__patch",
37
50
  "update_site_audit_finding_forms_site_audit_findings__finding_id__patch"
38
51
  ],
@@ -133,6 +146,95 @@
133
146
  "read",
134
147
  "write"
135
148
  ]
149
+ },
150
+ {
151
+ "name": "FormActivity",
152
+ "file": "components/form-activity.tsx",
153
+ "purpose": "Audit trail of one form and the pages it appears on.",
154
+ "operations": [
155
+ "get_form_activity_forms__form_key__activity_get",
156
+ "get_form_placements_forms__form_key__placements_get"
157
+ ],
158
+ "tiers": [
159
+ "read"
160
+ ]
161
+ },
162
+ {
163
+ "name": "FormBackfill",
164
+ "file": "components/form-backfill.tsx",
165
+ "purpose": "Deliver earlier captured leads through the form route: preview, then a confirmed delivery.",
166
+ "operations": [
167
+ "get_form_overview_forms__form_key__overview_get",
168
+ "get_form_backfill_forms__form_key__backfills__job_id__get",
169
+ "create_form_backfill_forms__form_key__backfills_post"
170
+ ],
171
+ "tiers": [
172
+ "read",
173
+ "external"
174
+ ]
175
+ },
176
+ {
177
+ "name": "FormGroups",
178
+ "file": "components/form-groups.tsx",
179
+ "purpose": "Curated groups of forms: create, rename, add and remove forms, delete.",
180
+ "operations": [
181
+ "list_forms_forms_get",
182
+ "update_form_group_forms_groups__group_id__patch",
183
+ "add_form_group_members_forms_groups__group_id__members_post",
184
+ "remove_form_group_members_forms_groups__group_id__members_remove_post",
185
+ "delete_form_group_forms_groups__group_id__delete",
186
+ "list_form_groups_forms_groups_get",
187
+ "create_form_group_forms_groups_post"
188
+ ],
189
+ "tiers": [
190
+ "read",
191
+ "write",
192
+ "destructive"
193
+ ]
194
+ },
195
+ {
196
+ "name": "FormRouteHistory",
197
+ "file": "components/form-route-history.tsx",
198
+ "purpose": "Undo a routing change: restore the previous route or stop routing the form.",
199
+ "operations": [
200
+ "get_form_overview_forms__form_key__overview_get",
201
+ "rollback_form_route_forms__form_key__route_delete"
202
+ ],
203
+ "tiers": [
204
+ "read",
205
+ "destructive"
206
+ ]
207
+ },
208
+ {
209
+ "name": "FormSavedViews",
210
+ "file": "components/form-saved-views.tsx",
211
+ "purpose": "Save and reapply the forms list search and health filter.",
212
+ "operations": [
213
+ "list_saved_form_views_forms_views_saved_get",
214
+ "save_form_view_forms_views_saved__view_id__put"
215
+ ],
216
+ "tiers": [
217
+ "read",
218
+ "write"
219
+ ]
220
+ },
221
+ {
222
+ "name": "MoreRows",
223
+ "file": "components/more-rows.tsx",
224
+ "purpose": "Footer for paged lists: how many rows are shown and a Load more button.",
225
+ "operations": [],
226
+ "tiers": []
227
+ },
228
+ {
229
+ "name": "SubmissionsExport",
230
+ "file": "components/submissions-export.tsx",
231
+ "purpose": "Download a form submissions as a CSV file.",
232
+ "operations": [
233
+ "search_form_submissions_forms__form_key__submissions_post"
234
+ ],
235
+ "tiers": [
236
+ "read"
237
+ ]
136
238
  }
137
239
  ],
138
240
  "hooks": [],
@@ -140,6 +242,10 @@
140
242
  {
141
243
  "file": "lib/form-types.ts",
142
244
  "purpose": "Form/submission types and tolerant readers for passthrough responses, status labels, mapping payload builder."
245
+ },
246
+ {
247
+ "file": "lib/forms-extra.ts",
248
+ "purpose": "Helpers for the forms management components: open-dict readers, paging adapter, date and CSV helpers."
143
249
  }
144
250
  ],
145
251
  "recipes": [
@@ -187,6 +293,7 @@
187
293
  ],
188
294
  "scopes": [
189
295
  "contacts:read",
296
+ "forms:delete",
190
297
  "forms:read",
191
298
  "forms:run",
192
299
  "forms:write",
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FormActivity: the audit trail of one form (route saved, delivery paused, backfill queued...) and the pages it was
5
+ * seen on.
6
+ *
7
+ * SDK operations:
8
+ * 1. getFormActivity GET /forms/{form_key}/activity?source_id (read; newest first)
9
+ * 2. getFormPlacements GET /forms/{form_key}/placements?source_id (read; the page URLs the form was seen on)
10
+ *
11
+ * Both payloads are open dicts: events show whatever of `action`, `actor_id`, `created_at` and `details` is present.
12
+ * The API returns a bounded history (the latest events); when it comes back full the screen says so.
13
+ */
14
+ import { Skeleton } from "@graph8/sdk/ui";
15
+ import { ApiError } from "@/g8/components/api-error";
16
+ import { EmptyState } from "@/g8/components/empty-state";
17
+ import { SectionCard } from "@/g8/components/screen/section-card";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import type { FormRef } from "../lib/form-types";
21
+ import { rowsOf, str, when } from "../lib/forms-extra";
22
+ import type { Rec } from "../lib/forms-extra";
23
+
24
+ export interface FormActivityProps {
25
+ form: FormRef;
26
+ className?: string;
27
+ }
28
+
29
+ /** How many events the activity read is expected to hold at most; a full read gets a notice. */
30
+ export const ACTIVITY_WINDOW = 500;
31
+
32
+ const words = (v: unknown): string => str(v).replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
33
+
34
+ export function FormActivity({ form, className }: FormActivityProps) {
35
+ const base = `/forms/${encodeURIComponent(form.form_key)}`;
36
+ const q = { source_id: form.source_id };
37
+ const activity = useG8Query((signal) => g8Get(`${base}/activity`, q, { signal }), [form.form_key, form.source_id]);
38
+ const placements = useG8Query((signal) => g8Get(`${base}/placements`, q, { signal }), [form.form_key, form.source_id]);
39
+ const events = rowsOf<Rec>(activity.data?.data, "events", "activity");
40
+ const pages = rowsOf<Rec>(placements.data?.data, "placements");
41
+ return (
42
+ <div className={className}>
43
+ <div className="grid gap-4">
44
+ <SectionCard title="Where this form appears" description="Pages the form was seen on.">
45
+ {placements.error ? <ApiError error={placements.error} title="Could not load the pages" onRetry={() => void placements.refetch()} /> : placements.loading && !placements.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : pages.length === 0 ? (
46
+ <EmptyState compact title="No pages recorded yet" description="A page is recorded when the form is submitted from it." />
47
+ ) : (
48
+ <ul className="grid gap-1">{pages.map((p, i) => <li key={i} className="truncate text-sm">{str(p.page_url) || str(p.url) || str(p.path)}{p.last_seen_at ? <span className="text-muted-foreground text-xs"> · last seen {when(p.last_seen_at)}</span> : null}</li>)}</ul>
49
+ )}
50
+ </SectionCard>
51
+ <SectionCard title="Activity" description="Changes made to how this form is routed.">
52
+ {activity.error ? <ApiError error={activity.error} title="Could not load the activity" onRetry={() => void activity.refetch()} /> : activity.loading && !activity.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : events.length === 0 ? (
53
+ <EmptyState compact title="No activity yet" description="Changes to the destination, mapping and delivery are listed here." />
54
+ ) : (
55
+ <>
56
+ <ol className="grid gap-2">
57
+ {events.map((e, i) => (
58
+ <li key={str(e.id) || i} className="rounded-md border p-2 text-sm">
59
+ <span className="font-medium">{words(e.action) || words(e.type) || "Change"}</span>
60
+ <span className="text-muted-foreground text-xs"> {when(e.created_at)}{str(e.actor_id) ? ` · by ${str(e.actor_id)}` : ""}</span>
61
+ </li>
62
+ ))}
63
+ </ol>
64
+ {events.length >= ACTIVITY_WINDOW ? <p role="note" className="text-muted-foreground mt-2 text-xs">Showing the latest {ACTIVITY_WINDOW} events. Older ones are not listed.</p> : null}
65
+ </>
66
+ )}
67
+ </SectionCard>
68
+ </div>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FormBackfill: deliver leads that were captured BEFORE the form had a destination (or while it was paused) through the
5
+ * form's current route. Preview first, then deliver. Delivering creates or updates contacts and cannot be undone.
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. getFormOverview GET /forms/{form_key}/overview?source_id (read; the destination, the route version and how many leads were captured)
9
+ * 2. createFormBackfill POST /forms/{form_key}/backfills { source_id, start_at?, end_at?, preview: true } (records a PREVIEW job; delivers nothing)
10
+ * 3. createFormBackfill POST /forms/{form_key}/backfills { ..., preview: false, estimated } (EXTERNAL, confirmed: delivers leads)
11
+ * 4. getFormBackfill GET /forms/{form_key}/backfills/{job} (read; polled every 3 s while the job is queued or running)
12
+ *
13
+ * A form with no destination cannot be backfilled: the screen says so and points at "Destination and fields". The API
14
+ * does not count the leads in a window, so the confirm states the most that can be delivered (all captured leads) and
15
+ * the dates only narrow it. Dates are whole days in UTC: the end day is included.
16
+ */
17
+ import { useEffect, useState } from "react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
22
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
28
+ import type { FormOverview, FormRef } from "../lib/form-types";
29
+ import { asRec, dayInstant, num, str } from "../lib/forms-extra";
30
+
31
+ export interface FormBackfillProps {
32
+ form: FormRef;
33
+ className?: string;
34
+ }
35
+
36
+ const ACTIVE = new Set(["queued", "running"]);
37
+
38
+ export function FormBackfill({ form, className }: FormBackfillProps) {
39
+ const base = `/forms/${encodeURIComponent(form.form_key)}`;
40
+ const { ask, dialog, failure } = useConfirmAction();
41
+ const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, { source_id: form.source_id }, { signal }).then((r) => r.data), [form.form_key, form.source_id]);
42
+ const [from, setFrom] = useState("");
43
+ const [to, setTo] = useState("");
44
+ const [jobId, setJobId] = useState("");
45
+ const job = useG8Query((signal) => g8Get(`${base}/backfills/${encodeURIComponent(jobId)}`, undefined, { signal }), [jobId], { enabled: jobId !== "" });
46
+ const j = asRec(job.data?.data);
47
+ const state = str(j.state);
48
+
49
+ useEffect(() => {
50
+ if (jobId === "" || !ACTIVE.has(state)) return;
51
+ const t = setTimeout(() => void job.refetch(), 3000);
52
+ return () => clearTimeout(t);
53
+ }, [jobId, state, job.data]); // eslint-disable-line react-hooks/exhaustive-deps
54
+
55
+ const startAt = dayInstant(from);
56
+ const endAt = dayInstant(to, true);
57
+ const reversed = startAt !== undefined && endAt !== undefined && startAt >= endAt;
58
+ // A legacy-mapping form has a destination but no route: the API refuses a backfill for it (409), so gate on the route.
59
+ const hasDestination = Boolean(overview.data?.route);
60
+ const captured = overview.data?.metrics?.captured ?? 0;
61
+ const window = { ...(startAt ? { start_at: startAt } : {}), ...(endAt ? { end_at: endAt } : {}) };
62
+ const preview = useG8Mutation(async () => { const res = await g8Post(`${base}/backfills`, { source_id: form.source_id, ...window, preview: true }); setJobId(str(asRec(res.data).id)); });
63
+
64
+ const deliver = (): void => ask({
65
+ tier: "external",
66
+ title: `Deliver captured leads from "${form.form_key}"`,
67
+ confirmLabel: "Deliver leads",
68
+ recipients: captured,
69
+ description: `Runs the saved leads through the current destination${overview.data?.destination?.list_name ? ` (${overview.data.destination.list_name})` : ""}, creating or updating contacts. Up to ${captured.toLocaleString()} captured leads, fewer if you set dates. This cannot be undone.`,
70
+ run: async () => { const res = await g8Post(`${base}/backfills`, { source_id: form.source_id, ...window, preview: false, estimated: captured }); setJobId(str(asRec(res.data).id)); },
71
+ });
72
+
73
+ return (
74
+ <SectionCard className={className} title="Backfill earlier leads" description="Send leads captured before the destination was set up.">
75
+ {overview.error ? <ApiError error={overview.error} title="Could not load the form" onRetry={() => void overview.refetch()} /> : !overview.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : !hasDestination ? (
76
+ <p className="text-sm">This form has no destination route yet. Set one in Destination and fields, then come back.</p>
77
+ ) : (
78
+ <div className="grid gap-3">
79
+ <p className="text-sm">{captured.toLocaleString()} captured {captured === 1 ? "lead" : "leads"} are available to deliver to <strong>{overview.data.destination?.list_name ?? "the destination"}</strong>.</p>
80
+ <div className="flex flex-wrap items-end gap-3">
81
+ <div><Label htmlFor="fb-from" className="mb-1">From (optional)</Label><Input id="fb-from" type="date" value={from} onChange={(e) => setFrom(e.target.value)} /></div>
82
+ <div><Label htmlFor="fb-to" className="mb-1">Through (optional)</Label><Input id="fb-to" type="date" value={to} onChange={(e) => setTo(e.target.value)} aria-invalid={reversed} /></div>
83
+ <Button variant="outline" loading={preview.loading} disabled={reversed} onClick={() => void preview.mutate(undefined)}>Preview</Button>
84
+ <Button disabled={reversed || captured === 0 || ACTIVE.has(state)} onClick={deliver}>Deliver leads</Button>
85
+ </div>
86
+ {reversed ? <p role="alert" className="text-error-foreground text-xs">The start day must be before the end day.</p> : null}
87
+ {captured === 0 ? <p className="text-muted-foreground text-xs">Nothing has been captured yet.</p> : null}
88
+ {jobId !== "" ? (
89
+ job.error ? <ApiError error={job.error} title="Could not read the backfill" onRetry={() => void job.refetch()} /> : (
90
+ <div className="grid gap-2" role="status">
91
+ <p className="flex items-center gap-2 text-sm">Backfill <StatusPill tone={state === "completed" ? "positive" : state === "failed" ? "negative" : state === "preview" ? "neutral" : "info"}>{state || "starting"}</StatusPill></p>
92
+ <KeyValueList columns={2} items={[
93
+ { label: "Estimated leads", value: num(j.estimated) == null ? "" : String(j.estimated) },
94
+ { label: "Delivered", value: num(j.delivered) == null ? (num(j.processed) == null ? "" : String(j.processed)) : String(j.delivered) },
95
+ { label: "Failed", value: num(j.failed) == null ? "" : String(j.failed) },
96
+ ]} />
97
+ {state === "preview" ? <p className="text-muted-foreground text-xs">This was a preview. Nothing was delivered.</p> : null}
98
+ </div>
99
+ )
100
+ ) : null}
101
+ </div>
102
+ )}
103
+ {preview.error ? <ApiError error={preview.error} title="Could not preview" /> : null}
104
+ {failure ? <ApiError error={failure} title="Could not start the backfill" /> : null}
105
+ <ConfirmSpendDialog {...dialog} />
106
+ </SectionCard>
107
+ );
108
+ }
@@ -68,7 +68,7 @@ function FormHeader({ form, onChanged }: { form: FormListItem; onChanged: () =>
68
68
  ))}
69
69
  </dl>
70
70
  ) : null}
71
- {overview.data?.delivery_warning ? <p className="text-sm text-amber-700">{overview.data.delivery_warning}</p> : null}
71
+ {overview.data?.delivery_warning ? <p className="text-sm text-warning-foreground">{overview.data.delivery_warning}</p> : null}
72
72
  <ConfirmSpendDialog
73
73
  open={confirm !== null}
74
74
  tier="external"
@@ -75,7 +75,7 @@ export function FormEmbed({ streamId }: FormEmbedProps) {
75
75
  <select id="g8-stream" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" value={stream ?? s.stream_id ?? ""} onChange={(e) => setStream(e.target.value)}>
76
76
  {s.available_streams.map((st) => <option key={st.id} value={st.id}>{st.name}{st.domains?.length ? ` (${st.domains.join(", ")})` : ""}</option>)}
77
77
  </select>
78
- {s.warning ? <p className="text-sm text-amber-700">{s.warning}</p> : null}
78
+ {s.warning ? <p className="text-sm text-warning-foreground">{s.warning}</p> : null}
79
79
  </CardContent>
80
80
  </Card>
81
81
  ) : null}
@@ -0,0 +1,151 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FormGroups: curated groups of forms ("Demo requests", "Newsletter sign-ups") so related forms are read together.
5
+ * Create a group, rename or re-type it, add and remove its forms, delete it. A form belongs to one group: adding it to
6
+ * another group moves it.
7
+ *
8
+ * SDK operations, in the order the screen uses them:
9
+ * 1. listFormGroups GET /forms/groups (read)
10
+ * 2. listForms GET /forms?page&per_page&search (read; the forms you can add, paged with "Load more")
11
+ * 3. createFormGroup POST /forms/groups { name, form_type, members } (write)
12
+ * 4. updateFormGroup PATCH /forms/groups/{id} { name?, form_type? } (write; only changed fields are sent)
13
+ * 5. addFormGroupMembers POST /forms/groups/{id}/members { members } (write)
14
+ * 6. removeFormGroupMembers POST /forms/groups/{id}/members/remove { members } (write)
15
+ * 7. deleteFormGroup DELETE /forms/groups/{id} (destructive, confirmed: the forms themselves stay)
16
+ */
17
+ import { useState } from "react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
24
+ import { SectionCard } from "@/g8/components/screen/section-card";
25
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
26
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
27
+ import { usePaginated } from "@/g8/hooks/use-paginated";
28
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
29
+ import { formLabel, pickItems } from "../lib/form-types";
30
+ import type { FormListItem } from "../lib/form-types";
31
+ import { memberRef, pageOf, rowsOf, str } from "../lib/forms-extra";
32
+ import type { Rec } from "../lib/forms-extra";
33
+ import { MoreRows } from "./more-rows";
34
+
35
+ export interface FormGroupsProps {
36
+ className?: string;
37
+ }
38
+
39
+ const TYPES = ["lead", "newsletter", "meeting", "contact", "download", "other"] as const;
40
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
41
+ const PAGE = 50;
42
+
43
+ const keyOf = (f: { form_key: string; source_id: string }): string => `${f.source_id}/${f.form_key}`;
44
+
45
+ /** The forms that can be added, searchable and paged. Forms already picked are marked, never hidden. */
46
+ function FormPicker({ picked, onToggle }: { picked: Set<string>; onToggle: (f: FormListItem) => void }) {
47
+ const [search, setSearch] = useState("");
48
+ const list = usePaginated<FormListItem>(async (cursor, signal) => {
49
+ const page = cursor ? Number(cursor) : 1;
50
+ const res = await g8Get("/forms", { page, per_page: PAGE, search: search.trim() || undefined }, { signal });
51
+ const d = res.data as { items?: FormListItem[]; total?: number } | undefined;
52
+ const got = pickItems<FormListItem>(d, "items");
53
+ return pageOf(got, got.length > 0 && page * PAGE < (d?.total ?? 0) ? page + 1 : null);
54
+ }, [search]);
55
+ return (
56
+ <div className="grid gap-2">
57
+ <Input aria-label="Search forms to add" placeholder="Search forms" value={search} onChange={(e) => setSearch(e.target.value)} />
58
+ {list.error ? <ApiError error={list.error} title="Could not load forms" onRetry={() => void list.reload()} /> : list.loading ? <Skeleton className="h-16 w-full" aria-busy="true" /> : list.items.length === 0 ? (
59
+ <EmptyState compact title={search ? "No form matches" : "No forms captured yet"} />
60
+ ) : (
61
+ <>
62
+ <ul className="grid max-h-64 gap-1 overflow-y-auto">
63
+ {list.items.map((f) => (
64
+ <li key={keyOf(f)}>
65
+ <label className="flex items-center gap-2 rounded-md border p-2 text-sm">
66
+ <input type="checkbox" checked={picked.has(keyOf(f))} onChange={() => onToggle(f)} aria-label={`Include ${formLabel(f)}`} />
67
+ <span className="truncate">{formLabel(f)}</span>
68
+ </label>
69
+ </li>
70
+ ))}
71
+ </ul>
72
+ <MoreRows shown={list.items.length} noun="forms" hasMore={list.hasMore} loading={list.loadingMore} onMore={() => void list.loadMore()} />
73
+ </>
74
+ )}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ function GroupCard({ group, onChanged }: { group: Rec; onChanged: () => void }) {
80
+ const { ask, dialog, failure } = useConfirmAction();
81
+ const id = str(group.id);
82
+ const members = rowsOf<Rec>(group.members).map(memberRef);
83
+ const [name, setName] = useState(str(group.name));
84
+ const [type, setType] = useState(str(group.form_type, "other"));
85
+ const [adding, setAdding] = useState(false);
86
+ const [picked, setPicked] = useState<Map<string, { form_key: string; source_id: string }>>(new Map());
87
+ const changes: Rec = {};
88
+ if (name.trim() !== str(group.name)) changes.name = name.trim();
89
+ if (type !== str(group.form_type, "other")) changes.form_type = type;
90
+ const save = useG8Mutation(() => g8Patch(`/forms/groups/${encodeURIComponent(id)}`, changes), { onSuccess: onChanged });
91
+ const add = useG8Mutation(() => g8Post(`/forms/groups/${encodeURIComponent(id)}/members`, { members: [...picked.values()] }), { onSuccess: () => { setAdding(false); setPicked(new Map()); onChanged(); } });
92
+ const remove = useG8Mutation((m: { form_key: string; source_id: string }) => g8Post(`/forms/groups/${encodeURIComponent(id)}/members/remove`, { members: [m] }), { onSuccess: onChanged });
93
+ const toggle = (f: FormListItem): void => setPicked((cur) => { const n = new Map(cur); if (n.has(keyOf(f))) n.delete(keyOf(f)); else n.set(keyOf(f), { form_key: f.form_key, source_id: f.source_id }); return n; });
94
+ return (
95
+ <li className="grid gap-3 rounded-md border p-3">
96
+ <div className="flex flex-wrap items-center gap-2">
97
+ <Input aria-label={`Group name ${str(group.name)}`} className="max-w-xs" value={name} onChange={(e) => setName(e.target.value)} />
98
+ <select aria-label={`Type of ${str(group.name)}`} className={SELECT} value={type} onChange={(e) => setType(e.target.value)}>{TYPES.map((t) => <option key={t} value={t}>{t[0].toUpperCase() + t.slice(1)}</option>)}</select>
99
+ <Button size="sm" variant="outline" loading={save.loading} disabled={Object.keys(changes).length === 0 || name.trim() === ""} onClick={() => void save.mutate(undefined)}>Save group</Button>
100
+ <Button size="sm" variant="ghost" className="ml-auto" aria-label={`Delete group ${str(group.name)}`} onClick={() => ask({
101
+ tier: "destructive", title: `Delete the group "${str(group.name)}"`, confirmLabel: "Delete group", affected: members.length,
102
+ description: "Only the group is removed. Its forms keep capturing and routing leads.", tolerateStatus: [404],
103
+ run: () => g8Delete(`/forms/groups/${encodeURIComponent(id)}`), onDone: onChanged,
104
+ })}>Delete</Button>
105
+ </div>
106
+ {members.length === 0 ? <p className="text-muted-foreground text-xs">No forms in this group yet.</p> : (
107
+ <ul className="flex flex-wrap gap-1">{members.map((m) => (
108
+ <li key={keyOf(m)} className="flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs">{m.form_key}<button type="button" aria-label={`Remove ${m.form_key} from ${str(group.name)}`} className="text-muted-foreground hover:text-foreground" onClick={() => void remove.mutate(m)}>x</button></li>
109
+ ))}</ul>
110
+ )}
111
+ <div><Button size="sm" variant="outline" onClick={() => setAdding(true)}>Add forms</Button></div>
112
+ {[save.error, remove.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
113
+ {failure ? <ApiError error={failure} title="Could not delete the group" /> : null}
114
+ <FormSheet open={adding} onOpenChange={setAdding} title={`Add forms to ${str(group.name)}`} description="A form in another group moves here." width="lg"
115
+ footer={<><Button variant="outline" onClick={() => setAdding(false)}>Cancel</Button><Button loading={add.loading} disabled={picked.size === 0} onClick={() => void add.mutate(undefined)}>Add {picked.size || ""} forms</Button></>}>
116
+ <FormPicker picked={new Set(picked.keys())} onToggle={toggle} />
117
+ {add.error ? <ApiError error={add.error} title="Could not add the forms" /> : null}
118
+ </FormSheet>
119
+ <ConfirmSpendDialog {...dialog} />
120
+ </li>
121
+ );
122
+ }
123
+
124
+ export function FormGroups({ className }: FormGroupsProps) {
125
+ const groups = useG8Query((signal) => g8Get("/forms/groups", undefined, { signal }), []);
126
+ const rows = rowsOf<Rec>(groups.data?.data, "groups");
127
+ const [creating, setCreating] = useState(false);
128
+ const [name, setName] = useState("");
129
+ const [type, setType] = useState<(typeof TYPES)[number]>("lead");
130
+ const [picked, setPicked] = useState<Map<string, { form_key: string; source_id: string }>>(new Map());
131
+ const create = useG8Mutation(() => g8Post("/forms/groups", { name: name.trim(), form_type: type, members: [...picked.values()] }), {
132
+ onSuccess: () => { setCreating(false); setName(""); setPicked(new Map()); void groups.refetch(); },
133
+ });
134
+ const toggle = (f: FormListItem): void => setPicked((cur) => { const n = new Map(cur); if (n.has(keyOf(f))) n.delete(keyOf(f)); else n.set(keyOf(f), { form_key: f.form_key, source_id: f.source_id }); return n; });
135
+ return (
136
+ <div className={className}>
137
+ <SectionCard title="Form groups" description="Keep related forms together." actions={<Button size="sm" onClick={() => setCreating(true)}>New group</Button>}>
138
+ {groups.error ? <ApiError error={groups.error} title="Could not load groups" onRetry={() => void groups.refetch()} /> : groups.loading && !groups.data ? <Skeleton className="h-20 w-full" aria-busy="true" /> : rows.length === 0 ? (
139
+ <EmptyState compact title="No groups yet" description="Group forms such as every demo request form." action={<Button onClick={() => setCreating(true)}>New group</Button>} />
140
+ ) : <ul className="grid gap-3">{rows.map((g) => <GroupCard key={str(g.id)} group={g} onChanged={() => void groups.refetch()} />)}</ul>}
141
+ </SectionCard>
142
+ <FormSheet open={creating} onOpenChange={setCreating} title="New form group" description="Pick the forms that belong together." width="lg"
143
+ footer={<><Button variant="outline" onClick={() => setCreating(false)}>Cancel</Button><Button loading={create.loading} disabled={name.trim() === ""} onClick={() => void create.mutate(undefined)}>Create group</Button></>}>
144
+ <div><Label htmlFor="fg-name" className="mb-1">Name</Label><Input id="fg-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
145
+ <div><Label htmlFor="fg-type" className="mb-1">Type</Label><select id="fg-type" className={`${SELECT} w-full`} value={type} onChange={(e) => setType(e.target.value as (typeof TYPES)[number])}>{TYPES.map((t) => <option key={t} value={t}>{t[0].toUpperCase() + t.slice(1)}</option>)}</select></div>
146
+ <FormPicker picked={new Set(picked.keys())} onToggle={toggle} />
147
+ {create.error ? <ApiError error={create.error} title="Could not create the group" /> : null}
148
+ </FormSheet>
149
+ </div>
150
+ );
151
+ }