@graph8/sdk 0.370.0 → 0.374.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -12,6 +12,11 @@
12
12
  * or many (confirm, "N of M" report, 404 = already gone) - import a CSV/Excel file - review and merge duplicates -
13
13
  * add notes and tasks - hand a contact to other blocks through the slot props.
14
14
  *
15
+ * Record actions: assign an owner to a whole list (only with renderOwnerPicker, never for a selection), tag chips on the open
16
+ * contact plus add/remove tags on a selection and a tag manager, edit one field on a selection, verify emails and unlock
17
+ * locked contacts (both billable, behind ConfirmSpendDialog with the count taken from the selection), AI documents and
18
+ * signals/next best step tabs on the open contact. Each action also works from the row's "..." button.
19
+ *
15
20
  * SDK operations: none of its own. listContacts, getContact, getActivityFeed, duplicateCounts are `read`;
16
21
  * the forms, notes and tasks are `write`; deleteContact (rows, detail and bulk) is `destructive` and always goes
17
22
  * through ConfirmSpendDialog. Nothing here sends a message to a person.
@@ -22,7 +27,7 @@
22
27
  import { useEffect, useMemo, useState } from "react";
23
28
  import type { ReactNode } from "react";
24
29
  import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
25
- import { Building2, GitMerge, Lock, Plus, Trash2, Upload, Users } from "lucide-react";
30
+ import { Building2, GitMerge, Lock, Plus, Tags, Trash2, Upload, Users } from "lucide-react";
26
31
  import { ApiError } from "@/g8/components/api-error";
27
32
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
28
33
  import { EmptyState } from "@/g8/components/empty-state";
@@ -40,17 +45,30 @@ import { RecordLink } from "@/g8/components/screen/record-link";
40
45
  import { plural } from "@/g8/lib/plural";
41
46
  import { useG8Query } from "@/g8/hooks/use-g8-query";
42
47
  import { useUrlState } from "@/g8/hooks/use-url-state";
48
+ import { useUrlFilters } from "@/g8/hooks/use-url-filters";
49
+ import { ListSelect } from "./list-select";
50
+ import { MoreFilters, TextFilter } from "@/g8/components/screen/filter-controls";
43
51
  import { g8Delete, g8Get } from "@/g8/lib/g8-client";
44
- import { useDebouncedValue } from "../hooks/use-debounced";
45
52
  import { usePageQuery } from "../hooks/use-page-query";
46
- import { contactName, errorStatus, isMasked, location } from "../lib/crm-types";
53
+ import { contactName, contactSubtitle, errorStatus, isMasked, location } from "../lib/crm-types";
47
54
  import type { ContactRecord, ScreenActionContext } from "../lib/crm-types";
48
55
  import { Pager, SortHead, formatDay, sortRows, useBulkDelete, useRowSelection, useSort } from "../lib/screen-parts";
49
56
  import { ActivityTimeline } from "./activity-timeline";
50
57
  import { DedupeReview } from "./dedupe-review";
51
58
  import { ImportWizard } from "./import-wizard";
52
59
  import { NotesPanel } from "./notes-panel";
60
+ import { OwnerAssignListButton } from "./owner-assign";
61
+ import type { OwnerPickerProps } from "./owner-assign";
53
62
  import { RecordForm } from "./record-form";
63
+ import { BulkUpdateButton } from "./bulk-update";
64
+ import { ContactDocumentsTab } from "./contact-documents-tab";
65
+ import { ContactInsightsTab } from "./contact-insights-tab";
66
+ import { UnlockInfoButton } from "./unlock-info-button";
67
+ import { VerifyEmailButton } from "./verify-email-button";
68
+ import { ContactRowActions } from "./contact-row-actions";
69
+ import { ContactTagChips } from "./contact-tag-chips";
70
+ import { TagAssignButton } from "./tag-assign";
71
+ import { TagManager } from "./tag-manager";
54
72
  import { TaskList } from "./task-list";
55
73
 
56
74
  export interface ContactsScreenProps {
@@ -67,13 +85,22 @@ export interface ContactsScreenProps {
67
85
  toolbarActions?: ReactNode;
68
86
  /** Extra bulk actions next to Delete (add to list, enrich, enroll...). */
69
87
  renderBulkActions?: (selection: { ids: number[]; rows: ContactRecord[]; clear: () => void }) => ReactNode;
88
+ /**
89
+ * Team-member picker for "Assign owner to all N in this list" (pass the pickers block's MemberPicker:
90
+ * `renderOwnerPicker={(p) => <MemberPicker label="New owner" {...p} />}`). Injected so crm does not depend on pickers.
91
+ * Without it the action is hidden. It is offered only when a list is in scope with no other filter, and never for a selection:
92
+ * the API assigns owners by filter, not by id.
93
+ */
94
+ renderOwnerPicker?: (props: OwnerPickerProps) => ReactNode;
70
95
  /** Called with a company id from the contact panel ("Open company"). Without it the button is hidden. */
71
96
  onOpenCompany?: (companyId: number) => void;
72
97
  className?: string;
73
98
  }
74
99
 
75
- interface Filters { name: string; job_title: string; seniority_level: string; country: string }
76
- const NO_FILTERS: Filters = { name: "", job_title: "", seniority_level: "", country: "" };
100
+ /** Every filter the contacts API takes (name, email, job title, seniority, company name and id, country, state, city, department, industry), plus `list`. */
101
+ const FILTER_KEYS = ["name", "job_title", "seniority_level", "country", "email", "company_name", "company_id", "state", "city", "job_department", "industry", "list"] as const;
102
+ type FilterKey = (typeof FILTER_KEYS)[number];
103
+ const MORE_KEYS: readonly FilterKey[] = ["email", "company_name", "company_id", "state", "city", "job_department", "industry", "list"];
77
104
  const SENIORITY = ["C-Suite", "VP", "Director", "Manager"];
78
105
 
79
106
  function sortValue(row: ContactRecord, key: string): string | null | undefined {
@@ -81,6 +108,7 @@ function sortValue(row: ContactRecord, key: string): string | null | undefined {
81
108
  if (key === "title") return row.job_title;
82
109
  if (key === "company") return row.company_name;
83
110
  if (key === "location") return location(row);
111
+ if (key === "owner") return row.owner_name;
84
112
  return null;
85
113
  }
86
114
 
@@ -101,23 +129,28 @@ export function ContactsScreen(props: ContactsScreenProps) {
101
129
  const parsedId = idParam ? Number(idParam) : null;
102
130
  /** A malformed deep link (`?id=abc`) opens nothing instead of fetching /contacts/NaN. */
103
131
  const openId = parsedId != null && Number.isFinite(parsedId) ? parsedId : null;
104
- const [draft, setDraft] = useState<Filters>(NO_FILTERS);
105
- const filters = useDebouncedValue(draft);
132
+ // The filters live in the URL (`c_email=...`), so a filtered view survives reload, Back and sharing.
133
+ const urlFilters = useUrlFilters(FILTER_KEYS, { prefix: "c_" });
134
+ const { draft, filters } = urlFilters;
135
+ const apiFilters = Object.fromEntries(Object.entries(filters).filter(([k, v]) => v && k !== "list"));
136
+ const effectiveListId = listId ?? (filters.list ? Number(filters.list) : undefined);
106
137
  const [sheet, setSheet] = useState<"new" | "import" | "dupes" | null>(null);
107
138
  const [confirmDelete, setConfirmDelete] = useState(false);
139
+ const [tagManager, setTagManager] = useState(false);
108
140
  const [note, setNote] = useState<string | null>(null);
109
141
  const { sort, toggle: toggleSort } = useSort();
110
142
 
111
143
  const list = usePageQuery<ContactRecord>(
112
- (page, signal) => g8Get<ContactRecord[]>("/contacts", { page, limit: pageSize, list_id: listId, ...Object.fromEntries(Object.entries(filters).filter(([, v]) => v)) }, { signal }),
113
- [listId, filters],
144
+ (page, signal) => g8Get<ContactRecord[]>("/contacts", { page, limit: pageSize, list_id: effectiveListId, ...apiFilters }, { signal }),
145
+ [effectiveListId, filters],
114
146
  pageSize,
115
147
  ["contacts", "list", "screen"],
148
+ urlFilters.ready,
116
149
  );
117
150
  const dupes = useG8Query((signal) => g8Get<{ pending?: number; count?: number }>("/duplicates/counts", { entity_type: "contact" }, { signal }), [], { key: ["duplicates", "counts", "contact"] });
118
151
  const selection = useRowSelection<ContactRecord>(`${listId}|${JSON.stringify(filters)}`);
119
152
  const rows = useMemo(() => sortRows(list.rows, sort, sortValue), [list.rows, sort]);
120
- const hasFilters = Object.values(draft).some(Boolean);
153
+ const hasFilters = urlFilters.active;
121
154
  const pending = dupes.data?.data.pending ?? dupes.data?.data.count;
122
155
  const lockedOnPage = list.rows.filter((r) => isMasked(r.work_email)).length;
123
156
 
@@ -129,9 +162,11 @@ export function ContactsScreen(props: ContactsScreenProps) {
129
162
  void list.refetch();
130
163
  void dupes.refetch();
131
164
  });
165
+ /** The freshest copy of each selected row (a refetch after unlock or owner change replaces the stored one). */
166
+ const selectedRows = selection.ids.map((id) => list.rows.find((r) => r.id === id) ?? selection.selected.get(id)!).filter(Boolean);
132
167
  const allOnPage = rows.length > 0 && rows.every((r) => selection.selected.has(r.id));
133
168
  const reload = () => { void list.refetch(); void dupes.refetch(); };
134
- const set = (key: keyof Filters, v: string) => setDraft((f) => ({ ...f, [key]: v }));
169
+ const set = (key: FilterKey, v: string) => urlFilters.set(key, v);
135
170
 
136
171
  useEffect(() => { setNote(null); }, [filters, listId]);
137
172
 
@@ -144,6 +179,8 @@ export function ContactsScreen(props: ContactsScreenProps) {
144
179
  actions={
145
180
  <>
146
181
  {props.toolbarActions}
182
+ {effectiveListId != null && Object.keys(apiFilters).length === 0 && props.renderOwnerPicker ? <OwnerAssignListButton renderPicker={props.renderOwnerPicker} listId={effectiveListId} total={list.loading ? null : list.total} onDone={reload} /> : null}
183
+ <Button variant="outline" size="sm" onClick={() => setTagManager(true)}><Tags className="size-4" aria-hidden />Manage tags</Button>
147
184
  <Button variant="outline" size="sm" onClick={() => setSheet("dupes")}><GitMerge className="size-4" aria-hidden />Duplicates{pending ? ` (${pending})` : ""}</Button>
148
185
  <Button variant="outline" size="sm" onClick={() => setSheet("import")}><Upload className="size-4" aria-hidden />Import</Button>
149
186
  <Button size="sm" onClick={() => setSheet("new")}><Plus className="size-4" aria-hidden />New contact</Button>
@@ -173,7 +210,17 @@ export function ContactsScreen(props: ContactsScreenProps) {
173
210
  {SENIORITY.map((s) => <option key={s} value={s}>{s}</option>)}
174
211
  </select>
175
212
  <input aria-label="Filter by country" placeholder="Country" className="h-9 w-32 rounded-md border bg-background px-3 text-sm" value={draft.country} onChange={(e) => set("country", e.target.value)} />
176
- {hasFilters ? <Button variant="ghost" size="sm" onClick={() => setDraft(NO_FILTERS)}>Clear filters</Button> : null}
213
+ <MoreFilters activeCount={MORE_KEYS.filter((k) => draft[k]).length}>
214
+ <TextFilter label="email" type="email" value={draft.email} onChange={(v) => set("email", v)} className="w-44" />
215
+ <TextFilter label="company name" placeholder="Company" value={draft.company_name} onChange={(v) => set("company_name", v)} />
216
+ <TextFilter label="company ID" placeholder="Company ID" type="number" value={draft.company_id} onChange={(v) => set("company_id", v)} className="w-28" />
217
+ <TextFilter label="industry" value={draft.industry} onChange={(v) => set("industry", v)} />
218
+ <TextFilter label="department" placeholder="Department" value={draft.job_department} onChange={(v) => set("job_department", v)} />
219
+ <TextFilter label="state" value={draft.state} onChange={(v) => set("state", v)} className="w-28" />
220
+ <TextFilter label="city" value={draft.city} onChange={(v) => set("city", v)} className="w-32" />
221
+ {listId == null ? <ListSelect label="Filter by list" emptyOption="Any list" value={draft.list} valueLabel={`List ${draft.list}`} pageSize={100} onChange={(l) => set("list", l ? String(l.id) : "")} /> : null}
222
+ </MoreFilters>
223
+ {hasFilters ? <Button variant="ghost" size="sm" onClick={urlFilters.clear}>Clear filters</Button> : null}
177
224
  </>
178
225
  }
179
226
  />
@@ -182,6 +229,10 @@ export function ContactsScreen(props: ContactsScreenProps) {
182
229
  <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm" role="status">
183
230
  <span className="tabular-nums">{selection.ids.length} selected</span>
184
231
  {props.renderBulkActions?.({ ids: selection.ids, rows: selection.rows, clear: selection.clear })}
232
+ <TagAssignButton contactIds={selection.ids} onDone={reload} />
233
+ <BulkUpdateButton contactIds={selection.ids} onDone={reload} />
234
+ <VerifyEmailButton rows={selectedRows} onDone={reload} />
235
+ <UnlockInfoButton rows={selectedRows} onDone={reload} />
185
236
  <Button variant="destructive" size="sm" onClick={() => setConfirmDelete(true)}><Trash2 className="size-4" aria-hidden />Delete {selection.ids.length}</Button>
186
237
  <Button variant="ghost" size="sm" onClick={selection.clear}>Clear selection</Button>
187
238
  </div>
@@ -199,12 +250,13 @@ export function ContactsScreen(props: ContactsScreenProps) {
199
250
  <SortHead label="Company" sortKey="company" sort={sort} onSort={toggleSort} />
200
251
  <TableHead>Email</TableHead>
201
252
  <SortHead label="Location" sortKey="location" sort={sort} onSort={toggleSort} />
202
- {props.renderRowActions ? <TableHead className="w-8"><span className="sr-only">Actions</span></TableHead> : null}
253
+ <SortHead label="Owner" sortKey="owner" sort={sort} onSort={toggleSort} />
254
+ <TableHead className="w-8"><span className="sr-only">Actions</span></TableHead>
203
255
  </TableRow>
204
256
  </TableHeader>
205
257
  <TableBody>
206
258
  {list.loading && rows.length === 0
207
- ? Array.from({ length: 6 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={6 + (props.renderRowActions ? 1 : 0)}><Skeleton className="h-6 w-full" /></TableCell></TableRow>)
259
+ ? Array.from({ length: 6 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={8}><Skeleton className="h-6 w-full" /></TableCell></TableRow>)
208
260
  : rows.map((row) => {
209
261
  const name = contactName(row);
210
262
  return (
@@ -219,7 +271,10 @@ export function ContactsScreen(props: ContactsScreenProps) {
219
271
  <TableCell><Cell value={row.company_name} /></TableCell>
220
272
  <TableCell><Cell value={row.work_email} /></TableCell>
221
273
  <TableCell><Cell value={location(row)} /></TableCell>
222
- {props.renderRowActions ? <TableCell onClick={(e) => e.stopPropagation()}>{props.renderRowActions(row, { reload })}</TableCell> : null}
274
+ <TableCell><Cell value={row.owner_name} /></TableCell>
275
+ <TableCell onClick={(e) => e.stopPropagation()}>
276
+ <span className="flex items-center gap-1">{props.renderRowActions?.(row, { reload })}<ContactRowActions row={row} onDone={reload} /></span>
277
+ </TableCell>
223
278
  </TableRow>
224
279
  );
225
280
  })}
@@ -229,7 +284,7 @@ export function ContactsScreen(props: ContactsScreenProps) {
229
284
  <EmptyState
230
285
  title={hasFilters ? "No contacts match these filters" : "No contacts yet"}
231
286
  description={hasFilters ? "Try fewer filters." : "Add your first contact or import a file."}
232
- action={hasFilters ? <Button variant="outline" onClick={() => setDraft(NO_FILTERS)}>Clear filters</Button> : <Button onClick={() => setSheet("new")}>New contact</Button>}
287
+ action={hasFilters ? <Button variant="outline" onClick={urlFilters.clear}>Clear filters</Button> : <Button onClick={() => setSheet("new")}>New contact</Button>}
233
288
  />
234
289
  ) : null}
235
290
  <Pager page={list.page} pageCount={list.pageCount} total={list.total} noun="contacts" hasNext={list.hasNext} loading={list.loading} onPage={list.setPage} />
@@ -255,6 +310,7 @@ export function ContactsScreen(props: ContactsScreenProps) {
255
310
  <DedupeReview entity="contact" onChanged={reload} />
256
311
  </FormSheet>
257
312
 
313
+ <TagManager open={tagManager} onOpenChange={setTagManager} onChanged={reload} />
258
314
  <ConfirmSpendDialog
259
315
  open={confirmDelete}
260
316
  tier="destructive"
@@ -326,7 +382,7 @@ function ContactPanel({ full, setFull, contactId, seed, onClose, onChanged, onDe
326
382
  <>
327
383
  <DetailPanel
328
384
  title={name}
329
- subtitle={[contact.job_title, contact.company?.name ?? contact.company_name].filter(Boolean).join(" at ") || "No title or company"}
385
+ subtitle={contactSubtitle(contact)}
330
386
  avatar={<EntityAvatar name={name} kind="person" size="lg" />}
331
387
  badges={isMasked(contact.work_email) ? <StatusPill tone="warning" dot>Locked email</StatusPill> : undefined}
332
388
  onClose={onClose}
@@ -334,6 +390,7 @@ function ContactPanel({ full, setFull, contactId, seed, onClose, onChanged, onDe
334
390
  onCollapse={full ? () => setFull(false) : undefined}
335
391
  actions={
336
392
  <>
393
+ {partial ? null : <><VerifyEmailButton rows={[contact]} onDone={() => { void query.refetch(); onChanged(); }} /><UnlockInfoButton rows={[contact]} onDone={() => { void query.refetch(); onChanged(); }} /></>}
337
394
  {partial ? null : renderActions?.(contact, { reload: () => { void query.refetch(); bump(); onChanged(); } })}
338
395
  {contact.company?.id && onOpenCompany ? <Button variant="outline" size="sm" onClick={() => onOpenCompany(contact.company!.id as number)}><Building2 className="size-4" aria-hidden />Open company</Button> : null}
339
396
  <Button variant="outline" size="sm" disabled={partial} title={partial ? "Loading the full record" : undefined} onClick={() => setEditing(true)}>Edit</Button>
@@ -341,10 +398,12 @@ function ContactPanel({ full, setFull, contactId, seed, onClose, onChanged, onDe
341
398
  </>
342
399
  }
343
400
  tabs={[
344
- { id: "overview", label: "Overview", content: overview },
401
+ { id: "overview", label: "Overview", content: <div className="flex flex-col gap-4"><ContactTagChips contactId={contactId} onChanged={onChanged} />{overview}</div> },
345
402
  { id: "activity", label: "Activity", content: <ActivityTimeline entityType="contact" entityId={contactId} refreshKey={feedKey} /> },
346
403
  { id: "notes", label: "Notes", content: <NotesPanel entity="contact" entityId={contactId} onChanged={bump} /> },
347
404
  { id: "tasks", label: "Tasks", content: <TaskList entity="contact" entityId={contactId} onChanged={bump} /> },
405
+ { id: "documents", label: "AI documents", content: <ContactDocumentsTab contactId={contactId} contactName={name} /> },
406
+ { id: "insights", label: "Insights", content: <ContactInsightsTab contactId={contactId} /> },
348
407
  ...(partial ? [] : extraTabs).map((t) => ({ id: t.id, label: t.label, content: t.render(contact) })),
349
408
  ]}
350
409
  />
@@ -21,6 +21,7 @@
21
21
  import { useState } from "react";
22
22
  import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@graph8/sdk/ui";
23
23
  import { ApiError } from "@/g8/components/api-error";
24
+ import { FirstNotice } from "@/g8/components/paging-notices";
24
25
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
26
  import { EmptyState } from "@/g8/components/empty-state";
26
27
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
@@ -53,6 +54,9 @@ function side(pair: Pair, s: "a" | "b", entity: "contact" | "company") {
53
54
  return { id: pair[`${s}_id`] as number, name: name || String([pair[`${s}_email`], pair[`${s}_domain`]].find((v) => v && !isMasked(v)) ?? `#${pair[`${s}_id`]}`), lines: lines.filter(Boolean).map((l) => (isMasked(l) ? "Locked" : String(l))) };
54
55
  }
55
56
 
57
+ /** The recent-merges API has no paging: a full read is announced under the list. */
58
+ const RECENT_LIMIT = 25;
59
+
56
60
  export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps) {
57
61
  const brand = useBrand();
58
62
  const plural = entity === "contact" ? "contacts" : "companies";
@@ -67,7 +71,7 @@ export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps
67
71
  const res = await g8Get<{ items: Pair[]; next_cursor?: string | null }>("/duplicates", { entity_type: entity, status: "pending", limit: 25, cursor }, { signal });
68
72
  return { data: res.data.items ?? [], pagination: { next_cursor: res.data.next_cursor ?? null } };
69
73
  }, [entity]);
70
- const recent = useG8Query((signal) => g8Get<MergeRow[]>("/duplicates/recent-merges", { entity_type: entity, limit: 25 }, { signal }), [entity]);
74
+ const recent = useG8Query((signal) => g8Get<MergeRow[]>("/duplicates/recent-merges", { entity_type: entity, limit: RECENT_LIMIT }, { signal }), [entity]);
71
75
  const [busy, setBusy] = useState<string | null>(null);
72
76
  const dropPick = (id: string | undefined) => id && setPicked((p) => { const n = new Set(p); n.delete(id); return n; });
73
77
  const refreshAll = () => { void pairs.reload(); void counts.refetch(); void recent.refetch(); onChanged?.(); };
@@ -145,6 +149,7 @@ export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps
145
149
  {m.can_undo ? <Button size="sm" variant="outline" loading={undo.loading && busy === m.id} onClick={() => void undo.mutate(m.id)}>Undo</Button> : null}
146
150
  </div>
147
151
  ))}
152
+ <FirstNotice shown={(recent.data?.data ?? []).length} limit={RECENT_LIMIT} noun="merges" />
148
153
  </TabsContent>
149
154
  </Tabs>
150
155
 
@@ -20,6 +20,7 @@
20
20
  import { useEffect, useState } from "react";
21
21
  import { Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
22
22
  import { ApiError } from "@/g8/components/api-error";
23
+ import { ListSelect } from "./list-select";
23
24
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
24
25
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
26
  import { useG8Query } from "@/g8/hooks/use-g8-query";
@@ -38,7 +39,6 @@ export interface ImportWizardProps {
38
39
  export interface ImportResult { imported?: number | null; updated?: number | null; skipped?: number | null; failed?: number | null; list_id?: number | null }
39
40
  interface Upload { datafile_id: string; row_count?: number | null; columns: string[] }
40
41
  interface Preview { rows: Array<Record<string, unknown>>; total: number }
41
- interface ListOption { id: number; title: string; type?: string | null }
42
42
 
43
43
  export const IMPORT_TARGETS: Array<{ value: string; label: string }> = [
44
44
  { value: "CONTACT_FIRST_NAME", label: "First name" }, { value: "CONTACT_LAST_NAME", label: "Last name" },
@@ -80,7 +80,6 @@ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, c
80
80
  const [confirmOpen, setConfirmOpen] = useState(false);
81
81
  const [result, setResult] = useState<ImportResult | null>(null);
82
82
 
83
- const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }), [], { enabled: upload != null && listId == null });
84
83
  const send = useG8Mutation((file: File) => { const body = new FormData(); body.append("file", file); return g8Post<Upload>("/imports", body); }, {
85
84
  onSuccess: (r) => { setUpload(r.data); setMapping(guessMapping(r.data.columns ?? [])); },
86
85
  });
@@ -107,7 +106,7 @@ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, c
107
106
  if (result) {
108
107
  return (
109
108
  <div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
110
- <h3 className="text-base font-semibold">Import finished</h3>
109
+ <h3 className="text-base font-medium">Import finished</h3>
111
110
  <p className="text-sm">{result.imported ?? 0} new, {result.updated ?? 0} updated, {result.skipped ?? 0} skipped, {result.failed ?? 0} failed.</p>
112
111
  <Button variant="outline" onClick={() => { setResult(null); setUpload(null); setMapping({}); setDestination(listId); setNewListTitle(""); setPolicy("autofill"); setEnrich(false); run.reset(); preview.reset(); }}>Import another file</Button>
113
112
  </div>
@@ -161,10 +160,7 @@ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, c
161
160
  <h3 className="text-sm font-medium">3. Destination and rules</h3>
162
161
  {listId == null ? (
163
162
  <div className="flex flex-wrap items-center gap-2">
164
- <select aria-label="Destination list" className="h-9 rounded-md border bg-background px-2 text-sm" value={destination ?? ""} onChange={(e) => setDestination(e.target.value ? Number(e.target.value) : undefined)}>
165
- <option value="">New list...</option>
166
- {(lists.data?.data ?? []).filter((l) => !l.type || l.type === "contacts").map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
167
- </select>
163
+ <ListSelect label="Destination list" emptyOption="New list..." type="contacts" pageSize={200} value={destination} valueLabel={newListTitle.trim() || undefined} enabled={upload != null && listId == null} selectClassName="h-9 rounded-md border bg-background px-2 text-sm" onChange={(l) => setDestination(l ? l.id : undefined)} />
168
164
  {destination == null ? <Input aria-label="New list name" placeholder="New list name" className="w-56" value={newListTitle} onChange={(e) => setNewListTitle(e.target.value)} /> : null}
169
165
  </div>
170
166
  ) : null}
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ListSelect: the audience-list dropdown with the list read wired in. A native select that is searchable (server
5
+ * search once there is more than one page) and paged ("Load more lists"), so the 201st list is reachable. It is the
6
+ * foundation's ListSelect plus `GET /lists` (the foundation holds no API scope); every block that picks a list
7
+ * (forms, lists, newsletter, nurture, sequencer, dialer) imports this one.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. GET /lists?search&page&limit (lists.listLists) read - through lib/read-lists.ts
11
+ */
12
+ import { ListSelect as BaseListSelect } from "@/g8/components/list-select";
13
+ import type { ListSelectProps } from "@/g8/components/list-select";
14
+ import { readLists } from "../lib/read-lists";
15
+
16
+ export type { ListSelectOption } from "@/g8/components/list-select";
17
+
18
+ export function ListSelect(props: Omit<ListSelectProps, "read">) {
19
+ return <BaseListSelect {...props} read={readLists} />;
20
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ /**
4
+ * OwnerAssignListButton: "Assign owner to all N contacts in this list". The ONLY owner action, and deliberately not a
5
+ * selection action.
6
+ *
7
+ * Why not for a selection: `POST /contacts/assign-owner` takes a FILTER (`list_id` + `filterModel`), not ids, and resolves
8
+ * it server-side across EVERY matching contact; its docstring warns the count is unbounded. Addressing a selection through
9
+ * an id filter would reassign the whole list or org if the key were ever ignored or renamed, so selections, rows and the
10
+ * open contact do not get an owner action here. There is no id-addressed owner write in the contract (bulk-update and
11
+ * PATCH take no owner), so that is an API gap: assign-owner needs an `ids` field.
12
+ *
13
+ * SDK operations, in order:
14
+ * 1. listTeamMembers GET /team-members via the injected picker (read, fetched when the picker opens)
15
+ * 2. assignContactOwner POST /contacts/assign-owner { owner_id, list_id, filterModel: {} } (write)
16
+ * Scope = the whole list, nothing else. N comes from the list's own count (the screen's total with no other filter
17
+ * applied), is shown in the confirm, and the button is disabled until that count has loaded. `owner_id: null`
18
+ * clears every owner in the list. Both paths go through ConfirmSpendDialog and name the list and N.
19
+ */
20
+
21
+ import { useState } from "react";
22
+ import type { ReactNode } from "react";
23
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
24
+ import { UserCog } from "lucide-react";
25
+ import { ApiError } from "@/g8/components/api-error";
26
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { g8Post } from "@/g8/lib/g8-client";
29
+ import { plural } from "@/g8/lib/plural";
30
+
31
+ /** A team member, as the injected picker reports it: the user id the API takes, and a name for the confirm. */
32
+ export interface OwnerChoice { id: string; name: string }
33
+
34
+ export interface OwnerPickerProps {
35
+ value: string | null;
36
+ onChange: (member: OwnerChoice) => void;
37
+ }
38
+
39
+ export interface OwnerAssignListButtonProps {
40
+ /**
41
+ * Renders the team-member picker (the pickers block's MemberPicker: `renderPicker={(p) => <MemberPicker label="New owner" {...p} />}`).
42
+ * It is injected so crm does not depend on pickers. Without it the button renders nothing: an owner is never typed as a raw id.
43
+ */
44
+ renderPicker?: (props: OwnerPickerProps) => ReactNode;
45
+ listId: number;
46
+ /** Shown in the confirm: "Q4 target accounts". Falls back to "list <id>". */
47
+ listName?: string | null;
48
+ /** The number of contacts in the list, from a real read. Null while it loads: the button stays disabled. */
49
+ total: number | null;
50
+ onDone?: (result: { updated: number | null; ownerId: string | null }) => void;
51
+ variant?: "default" | "outline" | "secondary" | "ghost";
52
+ size?: "default" | "sm";
53
+ className?: string;
54
+ }
55
+
56
+ /** `updated` from the answer: `{ updated }`, `{ data: { updated } }`; null when the API did not say. */
57
+ export function updatedCount(data: unknown): number | null {
58
+ const o = data as { updated?: unknown; data?: { updated?: unknown } } | null | undefined;
59
+ const n = o?.updated ?? o?.data?.updated;
60
+ return typeof n === "number" ? n : null;
61
+ }
62
+
63
+ export function OwnerAssignListButton({ renderPicker, listId, listName, total, onDone, variant = "outline", size = "sm", className }: OwnerAssignListButtonProps) {
64
+ const [open, setOpen] = useState(false);
65
+ const [member, setMember] = useState<OwnerChoice | null>(null);
66
+ const [pending, setPending] = useState<"assign" | "remove" | null>(null);
67
+ const [done, setDone] = useState<{ updated: number | null; total: number; removed: boolean } | null>(null);
68
+ const label = listName?.trim() || `list ${listId}`;
69
+ const n = total ?? 0;
70
+
71
+ const run = useG8Mutation(async (v: { ownerId: string | null; total: number }) => {
72
+ const res = await g8Post<unknown>("/contacts/assign-owner", { owner_id: v.ownerId, list_id: listId, filterModel: {} });
73
+ return { updated: updatedCount(res.data), ownerId: v.ownerId, total: v.total };
74
+ }, {
75
+ invalidates: [["contacts", "list"], ["contacts", "detail"], ["contacts", "record"]],
76
+ onSuccess: (r) => { setPending(null); setDone({ updated: r.updated, total: r.total, removed: r.ownerId == null }); onDone?.({ updated: r.updated, ownerId: r.ownerId }); },
77
+ });
78
+
79
+ const close = () => { setOpen(false); setMember(null); setDone(null); setPending(null); run.reset(); };
80
+ const short = done && done.updated != null && done.updated !== done.total;
81
+ const ready = total != null && total > 0;
82
+ if (!renderPicker) return null;
83
+
84
+ return (
85
+ <>
86
+ <Button className={className} variant={variant} size={size} disabled={!ready} title={total == null ? "Loading the list size" : total === 0 ? "This list has no contacts" : undefined} onClick={() => setOpen(true)}>
87
+ <UserCog className="size-4" aria-hidden />Assign owner to all{total != null ? ` ${total.toLocaleString()}` : ""}
88
+ </Button>
89
+ <Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
90
+ <DialogContent>
91
+ <DialogHeader>
92
+ <DialogTitle>Assign an owner to the whole list</DialogTitle>
93
+ <DialogDescription>This changes every contact in {label}, {plural(n, "contact")}. It does not use your selection.</DialogDescription>
94
+ </DialogHeader>
95
+ {done ? (
96
+ <div role="status" className="text-sm">
97
+ <p className="font-medium">{done.updated == null ? "Done." : `${done.removed ? "Removed the owner from" : "Assigned the owner to"} ${plural(done.updated, "contact")}.`}</p>
98
+ {short ? <p className="mt-1 text-destructive">The list had {done.total.toLocaleString()} contacts, but the server updated {done.updated!.toLocaleString()}.</p> : null}
99
+ </div>
100
+ ) : (
101
+ <>
102
+ {renderPicker?.({ value: member?.id ?? null, onChange: (m) => { setMember({ id: m.id, name: m.name }); run.reset(); } })}
103
+ {run.error && !pending ? <ApiError error={run.error} title="Could not change the owner" /> : null}
104
+ </>
105
+ )}
106
+ <DialogFooter>
107
+ {done ? <Button onClick={close}>Done</Button> : (
108
+ <>
109
+ <Button variant="ghost" onClick={close}>Cancel</Button>
110
+ <Button variant="outline" disabled={run.loading} onClick={() => setPending("remove")}>Remove all owners</Button>
111
+ <Button disabled={!member || run.loading} onClick={() => setPending("assign")}>Assign owner</Button>
112
+ </>
113
+ )}
114
+ </DialogFooter>
115
+ </DialogContent>
116
+ </Dialog>
117
+ <ConfirmSpendDialog
118
+ open={pending != null}
119
+ tier="destructive"
120
+ title={pending === "remove" ? `Remove the owner from all ${plural(n, "contact")} in ${label}` : `Make ${member?.name ?? "this person"} the owner of all ${plural(n, "contact")} in ${label}`}
121
+ description={pending === "remove"
122
+ ? "Every contact in the list will have no owner. The previous owners are not remembered, so this cannot be undone from here."
123
+ : "Every contact in the list gets this owner, replacing whoever owns it now. The previous owners are not remembered."}
124
+ affected={n}
125
+ confirmLabel={pending === "remove" ? "Remove owners" : "Assign owner"}
126
+ confirming={run.loading}
127
+ onConfirm={() => void run.mutate({ ownerId: pending === "remove" ? null : member?.id ?? null, total: n })}
128
+ onCancel={() => setPending(null)}
129
+ />
130
+ </>
131
+ );
132
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordEngagementTab: how much a contact or company has engaged: emails, calls, meetings, LinkedIn, website visits and
5
+ * downloads, the last touch, and the pages visited in the last 90 days. Use it as an `extraDetailTabs` entry of
6
+ * ContactsScreen / CompaniesScreen:
7
+ * { id: "engagement", label: "Engagement", render: (c) => <RecordEngagementTab kind="contacts" id={c.id} /> }
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. getContactEngagementSummary GET /contacts/{id}/engagement-summary (read) when kind="contacts"
11
+ * getCompanyEngagementSummary GET /companies/{id}/engagement-summary (read) when kind="companies"
12
+ * 2. getContactSitesVisited GET /contacts/{id}/sites-visited?days=90 (read) when kind="contacts"
13
+ * getCompanySitesVisited GET /companies/{id}/sites-visited?days=90 (read) when kind="companies"
14
+ * Nothing here is billable, external or destructive. The visits list is shown ten at a time with "Show more", so the
15
+ * count in the heading always matches what can be reached.
16
+ */
17
+
18
+ import { useState } from "react";
19
+ import { Button, Skeleton } from "@graph8/sdk/ui";
20
+ import { Activity } from "lucide-react";
21
+ import { ApiError } from "@/g8/components/api-error";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { g8Get } from "@/g8/lib/g8-client";
26
+
27
+ /** The counts both summaries share (the contact one adds LinkedIn and the last touch). */
28
+ interface Summary { emails?: number | null; calls?: number | null; meetings?: number | null; linkedin?: number | null; website_visits?: number | null; downloads?: number | null; contact_count?: number | null; last_contact?: string | null; last_contact_checked?: boolean | null }
29
+ interface Visit { url?: string | null; domain?: string | null; title?: string | null; event_date?: string | null; page_type?: string | null }
30
+ interface Visits { visits?: Visit[] | null; total_visits?: number | null }
31
+
32
+ const STEP = 10;
33
+ const DAYS = 90;
34
+
35
+ function dataOf<T>(env: unknown): T {
36
+ const d = (env as { data?: unknown } | null)?.data;
37
+ return (d && typeof d === "object" ? d : {}) as T;
38
+ }
39
+
40
+ function when(iso: string | null | undefined): string {
41
+ const d = iso ? new Date(iso) : null;
42
+ return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "";
43
+ }
44
+
45
+ const count = (n: number | null | undefined): string => (typeof n === "number" ? n.toLocaleString() : "-");
46
+
47
+ export interface RecordEngagementTabProps {
48
+ kind: "contacts" | "companies";
49
+ id: number;
50
+ }
51
+
52
+ export function RecordEngagementTab({ kind, id }: RecordEngagementTabProps) {
53
+ const [shown, setShown] = useState(STEP);
54
+ const summary = useG8Query(async (signal) => dataOf<Summary>(await g8Get(`/${kind}/${id}/engagement-summary`, undefined, { signal })), [kind, id]);
55
+ const visits = useG8Query(async (signal) => dataOf<Visits>(await g8Get(`/${kind}/${id}/sites-visited`, { days: DAYS }, { signal })), [kind, id]);
56
+
57
+ if (summary.error) return <ApiError error={summary.error} title="Could not load engagement" onRetry={() => void summary.refetch()} />;
58
+ if (!summary.data) return <Skeleton className="h-32 w-full" aria-label="Loading engagement" />;
59
+ const s = summary.data;
60
+ const items = [
61
+ { label: "Emails", value: count(s.emails) },
62
+ { label: "Calls", value: count(s.calls) },
63
+ { label: "Meetings", value: count(s.meetings) },
64
+ ...(kind === "contacts" ? [{ label: "LinkedIn", value: count(s.linkedin) }] : [{ label: "People", value: count(s.contact_count) }]),
65
+ { label: "Website visits", value: count(s.website_visits) },
66
+ { label: "Downloads", value: count(s.downloads) },
67
+ ...(kind === "contacts" ? [{ label: "Last touch", value: when(s.last_contact) || (s.last_contact_checked === false ? "Not checked" : "No touch yet") }] : []),
68
+ ];
69
+ const rows = visits.data?.visits ?? [];
70
+ const total = rows.length;
71
+ const reported = visits.data?.total_visits ?? total;
72
+ return (
73
+ <div className="space-y-4">
74
+ <KeyValueList columns={2} items={items} />
75
+ <section aria-label="Pages visited" className="space-y-2">
76
+ <h3 className="text-sm font-medium">Pages visited, last {DAYS} days</h3>
77
+ {visits.error ? <ApiError error={visits.error} title="Could not load pages visited" onRetry={() => void visits.refetch()} /> : null}
78
+ {!visits.error && !visits.data ? <Skeleton className="h-16 w-full" aria-label="Loading pages visited" /> : null}
79
+ {visits.data && total === 0 ? <EmptyState compact icon={<Activity className="size-8" />} title="No pages visited" description={`No website visits in the last ${DAYS} days.`} /> : null}
80
+ {total > 0 ? (
81
+ <>
82
+ <ul className="divide-y rounded-md border">
83
+ {rows.slice(0, shown).map((v, i) => (
84
+ <li key={`${v.url}-${i}`} className="flex items-center justify-between gap-3 p-3 text-sm">
85
+ <span className="min-w-0">
86
+ <span className="block truncate font-medium">{v.title || v.url || "Page"}</span>
87
+ <span className="block truncate text-xs text-muted-foreground">{[v.domain, v.page_type].filter(Boolean).join(" · ")}</span>
88
+ </span>
89
+ <span className="shrink-0 text-xs text-muted-foreground">{when(v.event_date)}</span>
90
+ </li>
91
+ ))}
92
+ </ul>
93
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
94
+ <span role="status">Showing {Math.min(shown, total).toLocaleString()} of {total.toLocaleString()} visits</span>
95
+ {shown < total ? <Button size="sm" variant="outline" onClick={() => setShown((n) => n + STEP)}>Show more</Button> : null}
96
+ </div>
97
+ {reported > total ? <p role="note" className="text-xs text-muted-foreground">The API returned {total.toLocaleString()} of {reported.toLocaleString()} visits in this window. Narrow it in the visitors screen to see the rest.</p> : null}
98
+ </>
99
+ ) : null}
100
+ </section>
101
+ </div>
102
+ );
103
+ }
@@ -178,7 +178,7 @@ export function RecordForm(props: RecordFormProps) {
178
178
 
179
179
  {validation ? <p role="alert" className="text-sm text-destructive">{validation}</p> : null}
180
180
  {duplicate ? (
181
- <div role="alert" className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm">
181
+ <div role="alert" className="rounded-md border border-warning-foreground/30 bg-warning p-3 text-sm">
182
182
  A contact with this email or LinkedIn URL already exists{existingId ? ` (id ${existingId})` : ""}.
183
183
  {props.onDuplicate ? <Button type="button" variant="link" size="sm" onClick={() => props.onDuplicate?.(existingId)}>Open the existing contact</Button> : null}
184
184
  </div>