@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
@@ -12,7 +12,8 @@
12
12
  "finished ListsScreen and SuppressionsScreen pages"
13
13
  ],
14
14
  "dependsOn": [
15
- "crm"
15
+ "crm",
16
+ "pickers"
16
17
  ],
17
18
  "operations": [
18
19
  "add_contacts_to_list_lists__list_id__contacts_post",
@@ -22,6 +23,7 @@
22
23
  "create_list_lists_post",
23
24
  "delete_list_lists__list_id__delete",
24
25
  "download_list_lists__audience_id__download_get",
26
+ "get_contact_lists_contacts__contact_id__lists_get",
25
27
  "list_contact_fields_fields_get",
26
28
  "list_contacts_contacts_get",
27
29
  "list_lists_lists_get",
@@ -86,7 +88,8 @@
86
88
  "list_suppressions_contacts_suppressions_get",
87
89
  "bulk_add_suppressions_contacts_suppressions_bulk_add_post",
88
90
  "check_contact_suppression_contacts__contact_id__suppression_get",
89
- "reinstate_suppressions_contacts_suppressions_reinstate_post"
91
+ "reinstate_suppressions_contacts_suppressions_reinstate_post",
92
+ "list_contacts_contacts_get"
90
93
  ],
91
94
  "tiers": [
92
95
  "read",
@@ -121,12 +124,37 @@
121
124
  "bulk_add_suppressions_contacts_suppressions_bulk_add_post",
122
125
  "check_contact_suppression_contacts__contact_id__suppression_get",
123
126
  "list_suppressions_contacts_suppressions_get",
124
- "reinstate_suppressions_contacts_suppressions_reinstate_post"
127
+ "reinstate_suppressions_contacts_suppressions_reinstate_post",
128
+ "list_contacts_contacts_get"
125
129
  ],
126
130
  "tiers": [
127
131
  "read",
128
132
  "write"
129
133
  ]
134
+ },
135
+ {
136
+ "name": "AddCompanyPeopleToListButton",
137
+ "file": "components/add-company-people-to-list-button.tsx",
138
+ "purpose": "\"Add people to list\" for one or more companies: reads the people at them, then AddToListButton picks the list and adds them.",
139
+ "operations": [
140
+ "list_contacts_contacts_get"
141
+ ],
142
+ "tiers": [
143
+ "read"
144
+ ]
145
+ },
146
+ {
147
+ "name": "RecordListsTab",
148
+ "file": "components/record-lists-tab.tsx",
149
+ "purpose": "The lists one contact belongs to, with Add to list and a confirmed Remove per list; an extraDetailTabs entry for ContactsScreen.",
150
+ "operations": [
151
+ "get_contact_lists_contacts__contact_id__lists_get",
152
+ "remove_contacts_from_list_lists__list_id__contacts_delete"
153
+ ],
154
+ "tiers": [
155
+ "read",
156
+ "destructive"
157
+ ]
130
158
  }
131
159
  ],
132
160
  "hooks": [],
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AddCompanyPeopleToListButton: "Add people to list" for one or more companies. Lists hold contacts, so this
5
+ * reads the people at the companies and hands them to AddToListButton's dialog (list picker, conflict review).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listContacts GET /contacts?company_id=&page=&limit=100 (read; every page, capped at 500 people)
9
+ * 2. then AddToListButton: listLists / createList, addContactsToList (write, chosen in the dialog)
10
+ *
11
+ * Reading people is free and adds nothing until the person picks a list and confirms in the dialog.
12
+ */
13
+
14
+ import { useState } from "react";
15
+ import { Button } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { g8Get } from "@/g8/lib/g8-client";
18
+ import { AddToListButton } from "./add-to-list-button";
19
+ import type { AddToListResult } from "./add-to-list-button";
20
+
21
+ export interface AddCompanyPeopleToListButtonProps {
22
+ companyIds: number[];
23
+ label?: string;
24
+ variant?: "default" | "outline" | "secondary" | "ghost";
25
+ size?: "default" | "sm";
26
+ disabled?: boolean;
27
+ onAdded?: (result: AddToListResult) => void;
28
+ className?: string;
29
+ }
30
+
31
+ const PEOPLE_CAP = 500;
32
+
33
+ interface PeopleEnvelope { data?: Array<{ id: number }>; pagination?: { has_next?: boolean } }
34
+
35
+ /** Ids of the people at these companies, every page, capped. */
36
+ async function peopleAt(companyIds: number[]): Promise<number[]> {
37
+ const ids = new Set<number>();
38
+ for (const companyId of companyIds) {
39
+ for (let page = 1; page <= 10 && ids.size < PEOPLE_CAP; page++) {
40
+ const env = (await g8Get<Array<{ id: number }>>("/contacts", { company_id: companyId, page, limit: 100 })) as PeopleEnvelope;
41
+ for (const c of env.data ?? []) ids.add(c.id);
42
+ if (!env.pagination?.has_next || (env.data ?? []).length === 0) break;
43
+ }
44
+ }
45
+ return [...ids].slice(0, PEOPLE_CAP);
46
+ }
47
+
48
+ export function AddCompanyPeopleToListButton({ companyIds, label = "Add people to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddCompanyPeopleToListButtonProps) {
49
+ const [loading, setLoading] = useState(false);
50
+ const [error, setError] = useState<unknown>(null);
51
+ const [people, setPeople] = useState<number[] | null>(null);
52
+
53
+ const start = async (): Promise<void> => {
54
+ setLoading(true);
55
+ setError(null);
56
+ try {
57
+ setPeople(await peopleAt(companyIds));
58
+ } catch (e) {
59
+ setError(e);
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ };
64
+
65
+ return (
66
+ <>
67
+ <Button className={className} variant={variant} size={size} loading={loading} disabled={disabled || companyIds.length === 0} onClick={() => void start()}>{label}</Button>
68
+ {error ? <ApiError error={error} title="Could not read the people" onRetry={() => void start()} /> : null}
69
+ {people && people.length === 0 ? <p role="status" className="text-xs text-muted-foreground">No people found at {companyIds.length === 1 ? "this company" : "these companies"}.</p> : null}
70
+ {people && people.length > 0 ? <AddToListButton key={people.join(",")} contactIds={people} defaultOpen hideTrigger onAdded={onAdded} onClosed={() => setPeople(null)} /> : null}
71
+ </>
72
+ );
73
+ }
@@ -42,6 +42,12 @@ export interface AddToListButtonProps {
42
42
  disabled?: boolean;
43
43
  onAdded?: (result: AddToListResult) => void;
44
44
  className?: string;
45
+ /** Start with the dialog open (used when another control resolved the contact ids first). */
46
+ defaultOpen?: boolean;
47
+ /** Render the dialog without its trigger button; pair with `defaultOpen` and `onClosed`. */
48
+ hideTrigger?: boolean;
49
+ /** Runs after the dialog closed (cancel, done or Escape). */
50
+ onClosed?: () => void;
45
51
  }
46
52
 
47
53
  interface ConflictDetail { code?: string; ready_count?: number; warned?: unknown[]; blocked?: unknown[]; message?: string }
@@ -52,26 +58,30 @@ function conflictOf(error: unknown): ConflictDetail | null {
52
58
  return e?.status === 409 && e.detail && typeof e.detail === "object" && e.detail.code === "CONFLICT_REVIEW_REQUIRED" ? e.detail : null;
53
59
  }
54
60
 
55
- export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddToListButtonProps) {
56
- const [open, setOpen] = useState(false);
61
+ export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className, defaultOpen = false, hideTrigger = false, onClosed }: AddToListButtonProps) {
62
+ const [open, setOpen] = useState(defaultOpen);
57
63
  const [target, setTarget] = useState<ListSummary | null>(null);
58
64
  const [result, setResult] = useState<AddToListResult | null>(null);
59
65
  const destination = listId ?? target?.id;
60
66
 
61
67
  const add = useG8Mutation(async (resolution?: Resolution) => {
62
- const res = await g8Post<{ added: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
68
+ const res = await g8Post<{ added?: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
63
69
  contact_ids: contactIds,
64
70
  ...(resolution ? { conflict_resolution: resolution } : {}),
65
71
  });
66
- return { listId: destination as number, added: res.data.added, already_member: res.data.already_member ?? [], not_found: res.data.not_found ?? [] };
72
+ const already = res.data.already_member ?? [];
73
+ const missing = res.data.not_found ?? [];
74
+ // The response is an open dict: when it carries no count, derive it instead of rendering "undefined".
75
+ const added = typeof res.data.added === "number" ? res.data.added : Math.max(contactIds.length - already.length - missing.length, 0);
76
+ return { listId: destination as number, added, already_member: already, not_found: missing };
67
77
  }, { invalidates: [["lists"], ["contacts", "list"]], onSuccess: (r) => { setResult(r); onAdded?.(r); } });
68
78
 
69
79
  const conflict = conflictOf(add.error);
70
- const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); };
80
+ const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); onClosed?.(); };
71
81
 
72
82
  return (
73
83
  <>
74
- <Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>
84
+ {hideTrigger ? null : <Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>}
75
85
  <Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
76
86
  <DialogContent>
77
87
  <DialogHeader>
@@ -89,7 +99,7 @@ export function AddToListButton({ contactIds, listId, label = "Add to list", var
89
99
  <>
90
100
  {listId == null ? <ListPicker value={target?.id} onChange={(l) => { setTarget(l); add.reset(); }} /> : null}
91
101
  {conflict ? (
92
- <div role="alert" className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm">
102
+ <div role="alert" className="rounded-md border border-warning-foreground/30 bg-warning p-3 text-sm">
93
103
  <p>{conflict.message ?? "Some contacts belong to other reps."}</p>
94
104
  <p className="mt-1">{conflict.ready_count ?? 0} ready, {conflict.warned?.length ?? 0} flagged, {conflict.blocked?.length ?? 0} blocked (never added).</p>
95
105
  <div className="mt-2 flex gap-2">
@@ -16,7 +16,9 @@ import { useEffect, useState } from "react";
16
16
  import { Button, Input } from "@graph8/sdk/ui";
17
17
  import { ApiError } from "@/g8/components/api-error";
18
18
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
19
- import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
20
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
21
+ import { LoadMore } from "@/g8/components/paging-notices";
20
22
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
21
23
 
22
24
  export interface ListSummary {
@@ -59,17 +61,24 @@ function useDebounced<T>(value: T, ms = 250): T {
59
61
  return v;
60
62
  }
61
63
 
64
+ /** Lists per read; "Load more" appends the next page (the picker used to stop silently at 50). */
65
+ const LIST_PAGE = 50;
66
+
62
67
  export function ListPicker({ value, onChange, type = "contacts", allowCreate = true, suggestedName, className }: ListPickerProps) {
63
68
  const [search, setSearch] = useState("");
64
69
  const [newName, setNewName] = useState(suggestedName ?? "");
65
70
  const q = useDebounced(search.trim());
66
- const lists = useG8Query((signal) => g8Get<ListSummary[]>("/lists", { search: q || undefined, limit: 50 }, { signal }), [q], { key: ["lists", "picker", { q }] });
71
+ const lists = useLoadMoreList(byPage<ListSummary>(LIST_PAGE, async (page, signal) => {
72
+ const env = await g8Get<ListSummary[]>("/lists", { search: q || undefined, page, limit: LIST_PAGE }, { signal });
73
+ const rows = (env.data ?? []) as ListSummary[];
74
+ return { rows, hasNext: hasNextOf(env, LIST_PAGE, rows.length) };
75
+ }), [q]);
67
76
  const create = useG8Mutation((title: string) => g8Post<ListSummary>("/lists", { title, type }), {
68
77
  invalidates: [["lists"]],
69
78
  onSuccess: (res) => { setNewName(""); void lists.refetch(); onChange({ type, ...res.data, title: res.data.title ?? newName }); },
70
79
  });
71
80
 
72
- const rows = ((lists.data?.data ?? []) as ListSummary[]).filter((l) => !l.type || l.type === type);
81
+ const rows = (lists.data ?? []).filter((l) => !l.type || l.type === type);
73
82
  return (
74
83
  <div className={`flex flex-col gap-2 ${className ?? ""}`}>
75
84
  <Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} />
@@ -86,6 +95,7 @@ export function ListPicker({ value, onChange, type = "contacts", allowCreate = t
86
95
  </li>
87
96
  ))}
88
97
  </ul>
98
+ <LoadMore shown={rows.length} noun="lists" hasMore={lists.hasMore} loading={lists.loadingMore} onLoadMore={lists.loadMore} />
89
99
  {allowCreate ? (
90
100
  <form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate(newName.trim()); }}>
91
101
  <Input aria-label="New list name" placeholder="Or create a new list" value={newName} onChange={(e) => setNewName(e.target.value)} />
@@ -46,7 +46,7 @@ function count(l: ListSummary): number {
46
46
  return l.contact_count ?? l.company_count ?? l.total ?? 0;
47
47
  }
48
48
 
49
- const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-amber-600" };
49
+ const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-warning-foreground" };
50
50
 
51
51
  export function ListTable({ linkParam, pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
52
52
  const [search, setSearch] = useState("");
@@ -67,6 +67,8 @@ export interface ListsScreenProps {
67
67
  extraDetailTabs?: { id: string; label: string; render: (list: ListSummary) => ReactNode }[];
68
68
  /** Where company-list members are managed. Default: the graph8 app. */
69
69
  appUrl?: string;
70
+ /** Open a list member in your contacts screen; without it the member rows are not clickable. */
71
+ onOpenContact?: (contactId: number) => void;
70
72
  className?: string;
71
73
  }
72
74
 
@@ -99,7 +101,7 @@ function when(value?: string | null): string {
99
101
  return value ? new Date(value).toLocaleDateString() : "";
100
102
  }
101
103
 
102
- export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, appUrl: appUrlProp, className }: ListsScreenProps) {
104
+ export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, appUrl: appUrlProp, onOpenContact, className }: ListsScreenProps) {
103
105
  const brand = useBrand();
104
106
  const appUrl = appUrlProp ?? brand.appUrl;
105
107
  const [search, setSearch] = useState("");
@@ -249,7 +251,7 @@ export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, r
249
251
  : (
250
252
  <div className="flex flex-col gap-2">
251
253
  {removeMembers.error ? <ApiError error={removeMembers.error} title="Could not remove them" /> : null}
252
- <ContactTable listId={selected.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey}
254
+ <ContactTable listId={selected.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey} onOpen={onOpenContact ? (c) => onOpenContact(c.id) : undefined}
253
255
  renderBulkActions={({ ids }) => <Button size="sm" variant="outline" onClick={() => setRemoving(ids)}>Remove from list</Button>} />
254
256
  </div>
255
257
  ),
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordListsTab: the lists one contact belongs to, with "Add to list" and a confirmed "Remove" per list. Use it as an
5
+ * `extraDetailTabs` entry of ContactsScreen:
6
+ * { id: "lists", label: "Lists", render: (c) => <RecordListsTab contactId={c.id} onOpenList={(id) => router.push(`/lists?id=${id}`)} /> }
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getContactLists GET /contacts/{id}/lists (read; the API answers every list the key can see, unpaged)
10
+ * 2. AddToListButton: listLists / createList, addContactsToList (write, chosen in its dialog)
11
+ * 3. removeContactsFromList DELETE /lists/{list_id}/contacts (destructive: confirmed with ConfirmSpendDialog)
12
+ * Removing takes the contact out of that list only; the contact record and its other lists are untouched.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ListChecks } from "lucide-react";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
24
+ import { AddToListButton } from "./add-to-list-button";
25
+
26
+ /** One row of GET /contacts/{id}/lists. */
27
+ interface MemberList { audience_id?: number | null; audience_title?: string | null }
28
+
29
+ /** The rows of an API answer, whether it came as `{ items }` or a bare array. */
30
+ function itemsOf<T>(env: unknown): T[] {
31
+ const data = (env as { data?: unknown } | null)?.data;
32
+ if (Array.isArray(data)) return data as T[];
33
+ const items = (data as { items?: unknown } | null)?.items;
34
+ return Array.isArray(items) ? (items as T[]) : [];
35
+ }
36
+
37
+ export interface RecordListsTabProps {
38
+ contactId: number;
39
+ /** Route to the list in your lists screen. Without it the rows link to `${listsPath}?id=<list id>`. */
40
+ onOpenList?: (listId: number) => void;
41
+ /** Path of your lists screen for the plain links. Default "/lists". */
42
+ listsPath?: string;
43
+ }
44
+
45
+ export function RecordListsTab({ contactId, onOpenList, listsPath = "/lists" }: RecordListsTabProps) {
46
+ const [removing, setRemoving] = useState<MemberList | null>(null);
47
+ const q = useG8Query(async (signal) => itemsOf<MemberList>(await g8Get(`/contacts/${contactId}/lists`, undefined, { signal })).filter((l) => l.audience_id != null), [contactId]);
48
+ const remove = useG8Mutation(async (list: MemberList) => {
49
+ await g8Delete(`/lists/${list.audience_id}/contacts`, { body: { contact_ids: [contactId] } });
50
+ setRemoving(null);
51
+ await q.refetch();
52
+ }, { invalidates: [["lists"], ["contacts", "list"]], onError: () => setRemoving(null) });
53
+
54
+ const add = <AddToListButton contactIds={[contactId]} onAdded={() => void q.refetch()} />;
55
+ if (q.error) return <ApiError error={q.error} title="Could not load lists" onRetry={() => void q.refetch()} />;
56
+ if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading lists" />;
57
+ return (
58
+ <div className="space-y-3">
59
+ <div className="flex items-center justify-between gap-2">
60
+ <p className="text-sm text-muted-foreground">{q.data.length === 1 ? "In 1 list" : `In ${q.data.length.toLocaleString()} lists`}</p>
61
+ {add}
62
+ </div>
63
+ {remove.error ? <ApiError error={remove.error} title="Could not remove from the list" /> : null}
64
+ {q.data.length === 0 ? (
65
+ <EmptyState compact icon={<ListChecks className="size-8" />} title="Not in any list" description="Add this contact to a list to sequence or sync it with others." />
66
+ ) : (
67
+ <ul className="divide-y rounded-md border" aria-label="Lists">
68
+ {q.data.map((l) => (
69
+ <li key={l.audience_id} className="flex items-center justify-between gap-3 p-3 text-sm">
70
+ <a
71
+ href={`${listsPath}?id=${encodeURIComponent(String(l.audience_id))}`}
72
+ className="min-w-0 truncate font-medium hover:underline"
73
+ onClick={(e) => {
74
+ if (!onOpenList || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
75
+ e.preventDefault();
76
+ onOpenList(l.audience_id as number);
77
+ }}
78
+ >
79
+ {l.audience_title || `List #${l.audience_id}`}
80
+ </a>
81
+ <Button size="sm" variant="ghost" loading={remove.loading && removing?.audience_id === l.audience_id} onClick={() => setRemoving(l)}>Remove</Button>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+ )}
86
+ <ConfirmSpendDialog
87
+ open={removing !== null}
88
+ tier="destructive"
89
+ title={`Remove from ${removing?.audience_title || "this list"}`}
90
+ description="The contact leaves this list only. The contact and its other lists are not changed."
91
+ affected={1}
92
+ confirmLabel="Remove"
93
+ confirming={remove.loading}
94
+ onConfirm={() => removing && void remove.mutate(removing)}
95
+ onCancel={() => setRemoving(null)}
96
+ />
97
+ </div>
98
+ );
99
+ }
@@ -11,6 +11,8 @@
11
11
  * 4. reinstateSuppressions POST /contacts/suppressions/reinstate { contact_ids, reason } (write, but it lets
12
12
  * sequences and campaigns contact people again, so it goes through ConfirmSpendDialog as "external")
13
13
  *
14
+ * No ids to type: contacts to suppress and the contact to check are chosen with a ContactPicker (search by name or work
15
+ * email; GET /contacts, read). Pasting numeric ids still works under "Paste ids instead" / "Check by id instead".
14
16
  * The ledger holds ids, not names or emails (no personal data): resolve a name with ContactDetail.
15
17
  * Suppressing only ever REDUCES outreach, so it runs without a confirmation.
16
18
  */
@@ -20,6 +22,8 @@ import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead,
20
22
  import { ApiError } from "@/g8/components/api-error";
21
23
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
24
  import { EmptyState } from "@/g8/components/empty-state";
25
+ import { ContactPicker } from "@/g8/pickers/components/contact-picker";
26
+ import type { ContactOption } from "@/g8/pickers/components/contact-picker";
23
27
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
28
  import { useG8Query } from "@/g8/hooks/use-g8-query";
25
29
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
@@ -59,6 +63,8 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
59
63
  const [category, setCategory] = useState("");
60
64
  const [offset, setOffset] = useState(0);
61
65
  const [idsText, setIdsText] = useState("");
66
+ const [chosen, setChosen] = useState<ContactOption[]>([]);
67
+ const [checkChosen, setCheckChosen] = useState<ContactOption | null>(null);
62
68
  const [addChannel, setAddChannel] = useState("all");
63
69
  const [reason, setReason] = useState("");
64
70
  const [picked, setPicked] = useState<Set<number>>(new Set());
@@ -68,7 +74,7 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
68
74
 
69
75
  const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
70
76
  const add = useG8Mutation((ids: number[]) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: ids, channel: addChannel, reason: reason.trim() || undefined }), {
71
- onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
77
+ onSuccess: () => { setIdsText(""); setChosen([]); setReason(""); void ledger.refetch(); },
72
78
  });
73
79
  const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
74
80
  onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); setPicked(new Set()); void ledger.refetch(); },
@@ -76,15 +82,23 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
76
82
  const check = useG8Mutation((id: number) => g8Get<ContactStatus>(`/contacts/${id}/suppression`));
77
83
 
78
84
  const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
79
- const ids = parseIds(idsText);
85
+ const ids = [...new Set([...chosen.map((c) => c.id), ...parseIds(idsText)])];
86
+ const checkTarget = checkChosen?.id ?? Number(checkId);
80
87
  const selectedContacts = [...picked];
81
88
  const togglePick = (contactId: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(contactId)) n.add(contactId); return n; });
82
89
 
83
90
  return (
84
91
  <div className={`flex flex-col gap-5 ${className ?? ""}`}>
85
92
  <section className="flex flex-col gap-2" aria-label="Suppress contacts">
86
- <Label htmlFor="suppress-ids">Suppress contacts by id</Label>
87
- <Textarea id="suppress-ids" rows={2} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
93
+ <span className="text-sm font-medium leading-none">Who to suppress</span>
94
+ <ContactPicker multiple label="Contacts to suppress" selected={chosen} onSelectedChange={setChosen} />
95
+ <details className="text-sm">
96
+ <summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
97
+ <div className="mt-1 flex flex-col gap-1">
98
+ <Label htmlFor="suppress-ids">Suppress contacts by id</Label>
99
+ <Textarea id="suppress-ids" rows={2} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
100
+ </div>
101
+ </details>
88
102
  <div className="flex flex-wrap items-center gap-2">
89
103
  <select aria-label="Channel to suppress" className="h-9 rounded-md border bg-background px-2 text-sm" value={addChannel} onChange={(e) => setAddChannel(e.target.value)}>
90
104
  <option value="all">All channels</option><option value="email">Email</option><option value="phone">Phone</option><option value="linkedin">LinkedIn</option>
@@ -97,8 +111,15 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
97
111
  </section>
98
112
 
99
113
  <section className="flex items-end gap-2" aria-label="Check a contact">
100
- <div className="flex flex-col gap-1"><Label htmlFor="check-id">Check one contact</Label><Input id="check-id" inputMode="numeric" placeholder="Contact id" className="w-40" value={checkId} onChange={(e) => setCheckId(e.target.value)} /></div>
101
- <Button variant="outline" size="sm" loading={check.loading} disabled={!Number(checkId)} onClick={() => void check.mutate(Number(checkId))}>Check</Button>
114
+ <div className="flex flex-col gap-1">
115
+ <span className="text-sm font-medium leading-none">Check one contact</span>
116
+ <ContactPicker label="Contact to check" value={checkChosen} onChange={(c) => { setCheckChosen(c); setCheckId(""); }} />
117
+ <details className="text-sm">
118
+ <summary className="cursor-pointer text-muted-foreground">Check by id instead</summary>
119
+ <div className="mt-1 flex flex-col gap-1"><Label htmlFor="check-id">Check one contact</Label><Input id="check-id" inputMode="numeric" placeholder="Contact id" className="w-40" value={checkId} onChange={(e) => { setCheckId(e.target.value); setCheckChosen(null); }} /></div>
120
+ </details>
121
+ </div>
122
+ <Button variant="outline" size="sm" loading={check.loading} disabled={!checkTarget} onClick={() => void check.mutate(checkTarget)}>Check</Button>
102
123
  {check.data ? <p role="status" className="text-sm">{check.data.data.is_suppressed ? `Suppressed on: ${check.data.data.active_channels.join(", ")}` : "Not suppressed"}</p> : null}
103
124
  {check.error ? <ApiError error={check.error} title="Could not check that contact" /> : null}
104
125
  </section>
@@ -9,9 +9,10 @@
9
9
  *
10
10
  * User flows:
11
11
  * - filter the ledger by channel and reason, page through it, see how many are suppressed on each channel
12
- * - Suppress contacts: paste ids, or pick contacts from the CRM, choose the channel (default all) and a reason
12
+ * - Suppress contacts: search and tick people (ContactPicker), or pick them from the CRM table, or paste ids under
13
+ * "Paste ids instead"; choose the channel (default all) and a reason
13
14
  * - open a ledger row: the contact's record plus its live suppression status (read-only check)
14
- * - check any contact id without opening it
15
+ * - check any contact without opening it (pick them by name; no ids to type)
15
16
  * - tick rows and Reinstate (confirmed: it lets sequences, campaigns and newsletters contact them again)
16
17
  *
17
18
  * SDK operations, in order:
@@ -24,7 +25,7 @@
24
25
 
25
26
  import { useEffect, useState } from "react";
26
27
  import type { ReactNode } from "react";
27
- import { BadgeCheck, Ban, Search, ShieldOff, Undo2 } from "lucide-react";
28
+ import { BadgeCheck, Ban, ShieldOff, Undo2 } from "lucide-react";
28
29
  import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
29
30
  import { ApiError } from "@/g8/components/api-error";
30
31
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
@@ -37,6 +38,8 @@ import { SectionCard } from "@/g8/components/screen/section-card";
37
38
  import { SplitView } from "@/g8/components/screen/split-view";
38
39
  import { StatusPill } from "@/g8/components/screen/status-pill";
39
40
  import { Toolbar } from "@/g8/components/screen/toolbar";
41
+ import { ContactPicker } from "@/g8/pickers/components/contact-picker";
42
+ import type { ContactOption } from "@/g8/pickers/components/contact-picker";
40
43
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
41
44
  import { useG8Query } from "@/g8/hooks/use-g8-query";
42
45
  import { useUrlState } from "@/g8/hooks/use-url-state";
@@ -85,6 +88,7 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
85
88
  const full = view === "full";
86
89
  const [adding, setAdding] = useState(false);
87
90
  const [idsText, setIdsText] = useState("");
91
+ const [pickedContacts, setPickedContacts] = useState<ContactOption[]>([]);
88
92
  const [addChannel, setAddChannel] = useState("all");
89
93
  const [reason, setReason] = useState("");
90
94
  const [picked, setPicked] = useState<Set<number>>(new Set());
@@ -92,8 +96,7 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
92
96
  /** Set when Reinstate is started from a row's panel: the confirm acts on that contact only and leaves the ticked rows alone. */
93
97
  const [panelTarget, setPanelTarget] = useState<number | null>(null);
94
98
  const [reinstateReason, setReinstateReason] = useState("");
95
- const [checkId, setCheckId] = useState("");
96
- const [checked, setChecked] = useState<number | null>(null);
99
+ const [checked, setChecked] = useState<ContactOption | null>(null);
97
100
 
98
101
  const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
99
102
  const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
@@ -101,13 +104,13 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
101
104
  useEffect(() => { setPicked((p) => new Set([...p].filter((id) => rows.some((r) => r.contact_id === id)))); }, [ledger.data]); // eslint-disable-line react-hooks/exhaustive-deps
102
105
 
103
106
  const add = useG8Mutation((v: { ids: number[] }) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: v.ids, channel: addChannel, reason: reason.trim() || undefined }), {
104
- onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
107
+ onSuccess: () => { setIdsText(""); setPickedContacts([]); setReason(""); void ledger.refetch(); },
105
108
  });
106
109
  const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
107
110
  onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); if (panelTarget != null) { const done = panelTarget; setPicked((p) => { const n = new Set(p); n.delete(done); return n; }); } else setPicked(new Set()); setPanelTarget(null); void ledger.refetch(); },
108
111
  });
109
112
 
110
- const ids = parseIds(idsText);
113
+ const ids = [...new Set([...pickedContacts.map((c) => c.id), ...parseIds(idsText)])];
111
114
  const chosen = panelTarget != null ? [panelTarget] : [...picked];
112
115
  const togglePick = (id: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(id)) n.add(id); return n; });
113
116
  const byChannel = (c: string) => rows.filter((r) => r.channel === c).length;
@@ -205,9 +208,8 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
205
208
  <select aria-label="Filter by reason" className={SELECT} value={category} onChange={(e) => { setCategory(e.target.value); resetPage(); }}>
206
209
  <option value="">Any reason</option><option value="contact_initiated">Contact asked</option><option value="org_initiated">Our team</option><option value="system">System</option>
207
210
  </select>
208
- <Input aria-label="Check a contact id" inputMode="numeric" placeholder="Check contact id" className="w-40" value={checkId} onChange={(e) => setCheckId(e.target.value)} />
209
- <Button variant="outline" size="sm" disabled={!Number(checkId)} onClick={() => setChecked(Number(checkId))}><Search className="mr-1 size-4" aria-hidden />Check</Button>
210
- {checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked} contactId={checked} /><span className="text-xs text-muted-foreground">#{checked}</span></span> : null}
211
+ <ContactPicker label="Contact to check" placeholder="Check a contact" value={checked} onChange={setChecked} />
212
+ {checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked.id} contactId={checked.id} /><span className="text-xs text-muted-foreground">{checked.name}</span></span> : null}
211
213
  </>)}
212
214
  actions={<Button variant="outline" size="sm" disabled={chosen.length === 0} onClick={() => { setPanelTarget(null); setConfirmReinstate(true); }}><BadgeCheck className="mr-1 size-4" aria-hidden />Reinstate {chosen.length || ""}</Button>}
213
215
  />
@@ -224,8 +226,15 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
224
226
  <div className="flex min-w-40 flex-1 flex-col gap-1"><Label htmlFor="suppress-reason">Reason (optional)</Label><Input id="suppress-reason" value={reason} onChange={(e) => setReason(e.target.value)} /></div>
225
227
  </div>
226
228
  <div className="flex flex-col gap-2">
227
- <Label htmlFor="suppress-ids">Contact ids</Label>
228
- <Textarea id="suppress-ids" rows={3} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
229
+ <span className="text-sm font-medium leading-none">Search and add people</span>
230
+ <ContactPicker multiple label="Contacts to suppress" selected={pickedContacts} onSelectedChange={setPickedContacts} />
231
+ <details className="text-sm">
232
+ <summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
233
+ <div className="mt-1 flex flex-col gap-1">
234
+ <Label htmlFor="suppress-ids">Contact ids</Label>
235
+ <Textarea id="suppress-ids" rows={3} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
236
+ </div>
237
+ </details>
229
238
  <Button size="sm" loading={add.loading} disabled={ids.length === 0} onClick={() => void add.mutate({ ids })}>{ids.length ? `Suppress ${plural(ids.length, "contact")}` : "Suppress contacts"}</Button>
230
239
  </div>
231
240
  <div className="flex flex-col gap-2">
@@ -157,6 +157,10 @@
157
157
  {
158
158
  "file": "lib/mailboxes-utils.ts",
159
159
  "purpose": "Mailbox and domain row types, credit estimate constants, status colors and idempotency-key helper."
160
+ },
161
+ {
162
+ "file": "lib/use-provider-domains.ts",
163
+ "purpose": "Every sending domain, read page after page (the domains API pages) instead of one capped call."
160
164
  }
161
165
  ],
162
166
  "recipes": [