@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
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ /**
4
+ * RecordOpenLinks: "Open contact" and "Open company" buttons for something that only knows ids (a call log entry, a
5
+ * website visitor). The ids are CHECKED against your CRM first, so a button is only shown for a record that exists
6
+ * and, when you pass `expectCompanyDomain`, for a company whose domain matches (event ids and CRM ids are not always
7
+ * the same id space). A record that cannot be loaded renders nothing, never a link to the wrong or a missing page.
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. getContact GET /contacts/{contact_id} (read; only with a numeric `contactId`)
11
+ * 2. getCompany GET /companies/{company_id} (read; only with a `companyId` that the contact did not already give)
12
+ * Nothing here is billable, external or destructive.
13
+ */
14
+
15
+ import { Button } from "@graph8/sdk/ui";
16
+ import { Building2, UserRound } from "lucide-react";
17
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
18
+ import { g8Get } from "@/g8/lib/g8-client";
19
+ import type { ContactRecord } from "../lib/crm-types";
20
+
21
+ export interface RecordOpenLinksProps {
22
+ /** A CRM contact id (number or numeric string). Anything else is ignored. */
23
+ contactId?: number | string | null;
24
+ /** A CRM company id. Optional: a contact that has a company supplies it. */
25
+ companyId?: number | string | null;
26
+ /** When set, a company is linked only if its domain equals this (case-insensitive). */
27
+ expectCompanyDomain?: string | null;
28
+ /** When set, the contact is linked only if its name equals this (case-insensitive, extra spaces ignored). */
29
+ expectContactName?: string | null;
30
+ onOpenContact?: (contactId: number) => void;
31
+ onOpenCompany?: (companyId: number) => void;
32
+ size?: "default" | "sm";
33
+ }
34
+
35
+ const idOf = (v: number | string | null | undefined): number | null => {
36
+ const s = String(v ?? "").trim();
37
+ return /^\d+$/.test(s) ? Number(s) : null;
38
+ };
39
+ const nameKey = (n: string | null | undefined): string => (n ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean).join(" ");
40
+ const domainOf = (d: string | null | undefined): string => (d ?? "").trim().toLowerCase().replace(/^www\./, "");
41
+
42
+ export function RecordOpenLinks({ contactId, companyId, expectCompanyDomain, expectContactName, onOpenContact, onOpenCompany, size = "sm" }: RecordOpenLinksProps) {
43
+ const cid = idOf(contactId);
44
+ const contact = useG8Query(
45
+ async (signal) => (await g8Get<ContactRecord>(`/contacts/${cid}`, undefined, { signal })).data ?? null,
46
+ [cid],
47
+ { enabled: cid != null && Boolean(onOpenContact || onOpenCompany), key: ["contacts", "open-links", cid] },
48
+ );
49
+ const c = contact.data;
50
+ // A contact whose name is not the expected one is some other person that happens to share the id: link nothing from it.
51
+ const contactOk = Boolean(c) && (!expectContactName || nameKey(expectContactName) === nameKey(c?.full_name || [c?.first_name, c?.last_name].filter(Boolean).join(" ")));
52
+ const fromContact = c && contactOk ? idOf(c.company_id ?? c.company?.id) : null;
53
+ const ownCompany = idOf(companyId);
54
+ const coId = fromContact ?? ownCompany;
55
+ // A company id that came from the caller (not from the contact) is verified by loading it.
56
+ const needsCheck = fromContact == null && ownCompany != null && Boolean(onOpenCompany);
57
+ const company = useG8Query(
58
+ async (signal) => (await g8Get<{ id?: number; domain?: string | null }>(`/companies/${ownCompany}`, undefined, { signal })).data ?? null,
59
+ [ownCompany],
60
+ { enabled: needsCheck && (cid == null || contact.data !== undefined || Boolean(contact.error)), key: ["companies", "open-links", ownCompany] },
61
+ );
62
+ const companyOk = fromContact != null || (needsCheck && company.data != null && (!expectCompanyDomain || domainOf(company.data.domain) === domainOf(expectCompanyDomain)));
63
+ return (
64
+ <>
65
+ {onOpenContact && contactOk && cid != null ? (
66
+ <Button size={size} variant="outline" onClick={() => onOpenContact(cid)}>
67
+ <UserRound className="mr-1 size-4" aria-hidden />
68
+ Open contact
69
+ </Button>
70
+ ) : null}
71
+ {onOpenCompany && coId != null && companyOk ? (
72
+ <Button size={size} variant="outline" onClick={() => onOpenCompany(coId)}>
73
+ <Building2 className="mr-1 size-4" aria-hidden />
74
+ Open company
75
+ </Button>
76
+ ) : null}
77
+ </>
78
+ );
79
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TagAssignButton: add tags to, or remove tags from, a selection of contacts. A dialog lists the org's tags as checkboxes;
5
+ * the person picks the tags and whether to add or remove them.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listContactTags GET /contacts/tags (read, via useContactTags, when opened)
9
+ * 2. assignContactTags POST /contacts/tags/assign { tag_ids, contact_ids } (write, idempotent)
10
+ * 3. unassignContactTags POST /contacts/tags/unassign { tag_ids, contact_ids } (write; tag definitions survive)
11
+ *
12
+ * One request for the whole selection (the API takes up to 200,000 contacts). The answer's `tagged` / `untagged` count is
13
+ * the number of contacts that changed; contacts that already had the tag are not counted, and the dialog says so.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
18
+ import { Tags } from "lucide-react";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { g8Post } from "@/g8/lib/g8-client";
22
+ import { plural } from "@/g8/lib/plural";
23
+ import { useContactTags } from "../hooks/use-contact-tags";
24
+ import { TagChip } from "./contact-tag-chips";
25
+
26
+ export interface TagAssignButtonProps {
27
+ contactIds: ReadonlyArray<number>;
28
+ onDone?: (result: { changed: number | null; mode: "add" | "remove" }) => void;
29
+ label?: string;
30
+ variant?: "default" | "outline" | "secondary" | "ghost";
31
+ size?: "default" | "sm";
32
+ className?: string;
33
+ defaultOpen?: boolean;
34
+ onClosed?: () => void;
35
+ }
36
+
37
+ type Mode = "add" | "remove";
38
+
39
+ export function TagAssignButton({ contactIds, onDone, label = "Tags", variant = "outline", size = "sm", className, defaultOpen = false, onClosed }: TagAssignButtonProps) {
40
+ const [open, setOpen] = useState(defaultOpen);
41
+ const [mode, setMode] = useState<Mode>("add");
42
+ const [picked, setPicked] = useState<ReadonlySet<string>>(new Set());
43
+ const [done, setDone] = useState<{ changed: number | null; mode: Mode; total: number } | null>(null);
44
+ const tags = useContactTags({ enabled: open });
45
+
46
+ const apply = useG8Mutation(async (v: { mode: Mode; tagIds: string[]; ids: number[] }) => {
47
+ const body = { tag_ids: v.tagIds, contact_ids: v.ids };
48
+ const res = v.mode === "add" ? await g8Post<Record<string, unknown>>("/contacts/tags/assign", body) : await g8Post<Record<string, unknown>>("/contacts/tags/unassign", body);
49
+ const n = res.data?.[v.mode === "add" ? "tagged" : "untagged"];
50
+ return { changed: typeof n === "number" ? n : null, mode: v.mode, total: v.ids.length };
51
+ }, {
52
+ invalidates: [["contact-tags"], ["contacts", "tags"], ["contacts", "list"]],
53
+ onSuccess: (r) => { setDone(r); onDone?.({ changed: r.changed, mode: r.mode }); },
54
+ });
55
+
56
+ const close = () => { setOpen(false); setPicked(new Set()); setDone(null); apply.reset(); onClosed?.(); };
57
+ const toggle = (id: string) => setPicked((s) => { const n = new Set(s); if (!n.delete(id)) n.add(id); return n; });
58
+ const count = contactIds.length;
59
+
60
+ return (
61
+ <>
62
+ {defaultOpen ? null : <Button className={className} variant={variant} size={size} disabled={count === 0} onClick={() => setOpen(true)}><Tags className="size-4" aria-hidden />{label}</Button>}
63
+ <Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
64
+ <DialogContent>
65
+ <DialogHeader>
66
+ <DialogTitle>Tag {plural(count, "contact")}</DialogTitle>
67
+ <DialogDescription>Tags group contacts outside lists. Adding a tag a contact already has changes nothing.</DialogDescription>
68
+ </DialogHeader>
69
+ {done ? (
70
+ <p role="status" className="text-sm font-medium">
71
+ {done.changed == null ? "Done." : done.mode === "add" ? `Tagged ${plural(done.changed, "contact")}.` : `Removed the tags from ${plural(done.changed, "contact")}.`}
72
+ {done.changed != null && done.changed < done.total ? <span className="block font-normal text-muted-foreground">{(done.total - done.changed).toLocaleString()} already matched and did not change.</span> : null}
73
+ </p>
74
+ ) : (
75
+ <>
76
+ <div role="radiogroup" aria-label="Action" className="flex gap-2">
77
+ {(["add", "remove"] as const).map((m) => (
78
+ <Button key={m} role="radio" aria-checked={mode === m} variant={mode === m ? "default" : "outline"} size="sm" onClick={() => { setMode(m); apply.reset(); }}>{m === "add" ? "Add tags" : "Remove tags"}</Button>
79
+ ))}
80
+ </div>
81
+ {tags.error ? <ApiError error={tags.error} title="Could not load tags" onRetry={() => void tags.refetch()} /> : null}
82
+ {tags.loading ? <p className="text-sm text-muted-foreground" role="status">Loading tags</p> : null}
83
+ {!tags.loading && !tags.error && tags.tags.length === 0 ? <p className="text-sm text-muted-foreground">No tags yet. Create one in Manage tags.</p> : null}
84
+ <ul className="flex max-h-64 flex-col gap-1 overflow-y-auto">
85
+ {tags.tags.map((t) => (
86
+ <li key={t.id}>
87
+ <label className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1 hover:bg-muted">
88
+ <input type="checkbox" checked={picked.has(t.id)} onChange={() => toggle(t.id)} aria-label={t.name} />
89
+ <TagChip tag={t} />
90
+ {t.contact_count != null ? <span className="ml-auto text-xs tabular-nums text-muted-foreground">{t.contact_count.toLocaleString()}</span> : null}
91
+ </label>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ {apply.error ? <ApiError error={apply.error} title="Could not change the tags" /> : null}
96
+ </>
97
+ )}
98
+ <DialogFooter>
99
+ {done ? <Button onClick={close}>Done</Button> : (
100
+ <>
101
+ <Button variant="ghost" onClick={close}>Cancel</Button>
102
+ <Button loading={apply.loading} disabled={picked.size === 0 || count === 0} onClick={() => void apply.mutate({ mode, tagIds: [...picked], ids: [...contactIds] })}>
103
+ {mode === "add" ? `Add ${plural(picked.size, "tag")}` : `Remove ${plural(picked.size, "tag")}`}
104
+ </Button>
105
+ </>
106
+ )}
107
+ </DialogFooter>
108
+ </DialogContent>
109
+ </Dialog>
110
+ </>
111
+ );
112
+ }
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TagManager: the org's contact tags in one sheet. Create, rename, recolour, archive and restore a tag, delete one
5
+ * (optionally moving its contacts to another tag first) and merge several tags into one.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listContactTags GET /contacts/tags?include_archived=true (read; every tag at once, with contact counts)
9
+ * 2. createContactTag POST /contacts/tags { name, color, description } (write; a duplicate name is refused by the API)
10
+ * 3. updateContactTag PATCH /contacts/tags/{tag_id} { name, color, description, archived } (write; omitted fields stay)
11
+ * 4. deleteContactTag DELETE /contacts/tags/{tag_id}?reassign_to=... (destructive, ConfirmSpendDialog)
12
+ * Without `reassign_to` the API drops the assignments together with the tag; the contacts themselves stay.
13
+ * 5. mergeContactTags POST /contacts/tags/merge { source_tag_ids, target_tag_id } (destructive: the sources are
14
+ * removed and their contacts get the target, ConfirmSpendDialog)
15
+ *
16
+ * Edit sends only what changed. A tag's description cannot be cleared (the API leaves a null field untouched), so
17
+ * the description is not offered as an editable field here; only name, colour and archive state are.
18
+ */
19
+
20
+ import { useMemo, useState } from "react";
21
+ import { Button, cn } from "@graph8/sdk/ui";
22
+ import { Archive, GitMerge, Pencil, Plus, Trash2 } from "lucide-react";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { g8Delete, g8Patch, g8Post } from "@/g8/lib/g8-client";
28
+ import { plural } from "@/g8/lib/plural";
29
+ import { useContactTags } from "../hooks/use-contact-tags";
30
+ import { HEX_COLOR, TAG_COLORS, isArchivedTag } from "../lib/record-actions";
31
+ import type { ContactTag } from "../lib/record-actions";
32
+ import { TagChip } from "./contact-tag-chips";
33
+
34
+ export interface TagManagerProps {
35
+ open: boolean;
36
+ onOpenChange: (open: boolean) => void;
37
+ /** Called after any change that can alter what the contact list shows. */
38
+ onChanged?: () => void;
39
+ }
40
+
41
+ function Swatches({ value, onChange, label }: { value: string; onChange: (c: string) => void; label: string }) {
42
+ return (
43
+ <span className="flex gap-1" role="radiogroup" aria-label={label}>
44
+ {TAG_COLORS.map((c) => <button key={c} type="button" role="radio" aria-checked={value === c} aria-label={`Colour ${c}`} onClick={() => onChange(c)} className={cn("size-5 rounded-full border-2", value === c ? "border-foreground" : "border-transparent")} style={{ backgroundColor: c }} />)}
45
+ </span>
46
+ );
47
+ }
48
+
49
+ export function TagManager({ open, onOpenChange, onChanged }: TagManagerProps) {
50
+ const all = useContactTags({ enabled: open, includeArchived: true });
51
+ const [name, setName] = useState("");
52
+ const [color, setColor] = useState<string>(TAG_COLORS[0]);
53
+ const [editing, setEditing] = useState<{ id: string; name: string; color: string } | null>(null);
54
+ const [picked, setPicked] = useState<ReadonlySet<string>>(new Set());
55
+ const [mergeInto, setMergeInto] = useState("");
56
+ const [confirmMerge, setConfirmMerge] = useState(false);
57
+ const [deleting, setDeleting] = useState<ContactTag | null>(null);
58
+ const [moveTo, setMoveTo] = useState("");
59
+ const done = () => { void all.refetch(); onChanged?.(); };
60
+ const keys = [["contact-tags"], ["contacts", "tags"], ["contacts", "list"]] as const;
61
+
62
+ const create = useG8Mutation((v: { name: string; color: string }) => g8Post("/contacts/tags", v), { invalidates: keys, onSuccess: () => { setName(""); done(); } });
63
+ const update = useG8Mutation((v: { id: string; body: Record<string, unknown> }) => g8Patch(`/contacts/tags/${v.id}`, v.body), { invalidates: keys, onSuccess: () => { setEditing(null); done(); } });
64
+ const remove = useG8Mutation((v: { id: string; reassignTo: string }) => g8Delete(`/contacts/tags/${v.id}`, v.reassignTo ? { query: { reassign_to: v.reassignTo } } : {}), {
65
+ invalidates: keys,
66
+ onSuccess: (_r, v) => { setDeleting(null); setMoveTo(""); setPicked((s) => { const n = new Set(s); n.delete(v.id); return n; }); done(); },
67
+ });
68
+ const merge = useG8Mutation((v: { sources: string[]; target: string }) => g8Post("/contacts/tags/merge", { source_tag_ids: v.sources, target_tag_id: v.target }), {
69
+ invalidates: keys,
70
+ onSuccess: () => { setConfirmMerge(false); setPicked(new Set()); setMergeInto(""); done(); },
71
+ });
72
+
73
+ const byId = useMemo(() => new Map(all.tags.map((t) => [t.id, t])), [all.tags]);
74
+ const sources = [...picked].filter((id) => byId.has(id));
75
+ const targets = all.tags.filter((t) => !picked.has(t.id) && !isArchivedTag(t));
76
+ const sourceContacts = sources.reduce((n, id) => n + (byId.get(id)?.contact_count ?? 0), 0);
77
+ const togglePick = (id: string) => setPicked((s) => { const n = new Set(s); if (!n.delete(id)) n.add(id); return n; });
78
+ const saveEdit = (t: ContactTag) => {
79
+ if (!editing) return;
80
+ const body: Record<string, unknown> = {};
81
+ if (editing.name.trim() && editing.name.trim() !== t.name) body.name = editing.name.trim();
82
+ if (HEX_COLOR.test(editing.color) && editing.color !== t.color) body.color = editing.color;
83
+ if (Object.keys(body).length === 0) { setEditing(null); return; }
84
+ void update.mutate({ id: t.id, body });
85
+ };
86
+ const deleteTargets = all.tags.filter((t) => t.id !== deleting?.id && !isArchivedTag(t));
87
+ const allCountsKnown = sources.every((id) => byId.get(id)?.contact_count != null);
88
+ const targetName = byId.get(mergeInto)?.name ?? "";
89
+
90
+ return (
91
+ <FormSheet open={open} onOpenChange={onOpenChange} title="Manage tags" description="Tags group contacts outside lists." width="lg" footer={<Button variant="ghost" onClick={() => onOpenChange(false)}>Close</Button>}>
92
+ <form className="flex flex-wrap items-center gap-2 border-b pb-3" onSubmit={(e) => { e.preventDefault(); if (name.trim()) void create.mutate({ name: name.trim(), color }); }}>
93
+ <input aria-label="Tag name" maxLength={50} placeholder="New tag" value={name} onChange={(e) => setName(e.target.value)} className="h-9 w-44 rounded-md border bg-background px-3 text-sm" />
94
+ <Swatches value={color} onChange={setColor} label="Tag colour" />
95
+ <Button type="submit" size="sm" loading={create.loading} disabled={!name.trim()}><Plus className="size-4" aria-hidden />Create tag</Button>
96
+ </form>
97
+ {create.error ? <ApiError error={create.error} title="Could not create the tag" /> : null}
98
+ {update.error ? <ApiError error={update.error} title="Could not save the tag" /> : null}
99
+ {all.error ? <ApiError error={all.error} title="Could not load tags" onRetry={() => void all.refetch()} /> : null}
100
+ {all.loading ? <p className="text-sm text-muted-foreground" role="status">Loading tags</p> : null}
101
+ {!all.loading && !all.error && all.tags.length === 0 ? <p className="py-6 text-center text-sm text-muted-foreground">No tags yet. Create the first one above.</p> : null}
102
+
103
+ {sources.length > 0 ? (
104
+ <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 p-2 text-sm" role="status">
105
+ <span>{plural(sources.length, "tag")} selected</span>
106
+ <select aria-label="Merge into" className="h-8 rounded-md border bg-background px-2" value={mergeInto} onChange={(e) => setMergeInto(e.target.value)}>
107
+ <option value="">Merge into...</option>
108
+ {targets.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
109
+ </select>
110
+ <Button size="sm" variant="outline" disabled={!mergeInto} onClick={() => setConfirmMerge(true)}><GitMerge className="size-4" aria-hidden />Merge</Button>
111
+ <Button size="sm" variant="ghost" onClick={() => setPicked(new Set())}>Clear</Button>
112
+ </div>
113
+ ) : null}
114
+ {merge.error ? <ApiError error={merge.error} title="Could not merge the tags" /> : null}
115
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the tag" /> : null}
116
+
117
+ <ul className="flex flex-col divide-y">
118
+ {all.tags.map((t) => (
119
+ <li key={t.id} className="flex flex-wrap items-center gap-2 py-2">
120
+ <input type="checkbox" aria-label={`Select ${t.name}`} checked={picked.has(t.id)} onChange={() => togglePick(t.id)} />
121
+ {editing?.id === t.id ? (
122
+ <form className="flex flex-wrap items-center gap-2" onSubmit={(e) => { e.preventDefault(); saveEdit(t); }}>
123
+ <input aria-label="Rename tag" maxLength={50} value={editing.name} onChange={(e) => setEditing({ ...editing, name: e.target.value })} className="h-8 w-40 rounded-md border bg-background px-2 text-sm" />
124
+ <Swatches value={editing.color} onChange={(c) => setEditing({ ...editing, color: c })} label="Edit colour" />
125
+ <Button type="submit" size="sm" loading={update.loading} disabled={!editing.name.trim()}>Save</Button>
126
+ <Button type="button" size="sm" variant="ghost" onClick={() => setEditing(null)}>Cancel</Button>
127
+ </form>
128
+ ) : (
129
+ <>
130
+ <span className={cn(isArchivedTag(t) && "opacity-60")}><TagChip tag={t} /></span>
131
+ {isArchivedTag(t) ? <span className="text-xs text-muted-foreground">Archived</span> : null}
132
+ <span className="text-xs tabular-nums text-muted-foreground">{t.contact_count == null ? "-" : plural(t.contact_count, "contact")}</span>
133
+ <span className="ml-auto flex gap-1">
134
+ <Button size="sm" variant="ghost" aria-label={`Edit ${t.name}`} onClick={() => setEditing({ id: t.id, name: t.name, color: t.color && HEX_COLOR.test(t.color) ? t.color : TAG_COLORS[0] })}><Pencil className="size-4" aria-hidden /></Button>
135
+ <Button size="sm" variant="ghost" aria-label={`${isArchivedTag(t) ? "Restore" : "Archive"} ${t.name}`} onClick={() => void update.mutate({ id: t.id, body: { archived: !isArchivedTag(t) } })}><Archive className="size-4" aria-hidden /></Button>
136
+ <Button size="sm" variant="ghost" aria-label={`Delete ${t.name}`} onClick={() => { setDeleting(t); setMoveTo(""); }}><Trash2 className="size-4" aria-hidden /></Button>
137
+ </span>
138
+ </>
139
+ )}
140
+ </li>
141
+ ))}
142
+ </ul>
143
+
144
+ <ConfirmSpendDialog
145
+ open={confirmMerge}
146
+ tier="destructive"
147
+ title={`Merge ${plural(sources.length, "tag")} into ${targetName}`}
148
+ description={`${sources.map((id) => byId.get(id)?.name).filter(Boolean).join(", ")} will be removed. ${allCountsKnown ? `Their ${plural(sourceContacts, "contact")} get` : "Their contacts get"} ${targetName} instead; contacts that already have it keep one copy.`}
149
+ affected={allCountsKnown ? sourceContacts : undefined}
150
+ confirmLabel="Merge tags"
151
+ confirming={merge.loading}
152
+ onConfirm={() => void merge.mutate({ sources, target: mergeInto })}
153
+ onCancel={() => setConfirmMerge(false)}
154
+ />
155
+ <ConfirmSpendDialog
156
+ open={deleting != null}
157
+ tier="destructive"
158
+ title={`Delete the tag ${deleting?.name ?? ""}`}
159
+ description={
160
+ <div className="flex flex-col gap-2">
161
+ <p>{moveTo ? `${deleting?.contact_count == null ? "Its contacts" : `Its ${plural(deleting.contact_count, "contact")}`} get ${byId.get(moveTo)?.name ?? "the other tag"} first.` : `It is removed from ${deleting?.contact_count == null ? "its contacts" : plural(deleting.contact_count, "contact")}. The contacts themselves stay.`}</p>
162
+ <label className="flex items-center gap-2 text-sm">Move its contacts to
163
+ <select aria-label="Move contacts to" className="h-8 rounded-md border bg-background px-2" value={moveTo} onChange={(e) => setMoveTo(e.target.value)}>
164
+ <option value="">No other tag</option>
165
+ {deleteTargets.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
166
+ </select>
167
+ </label>
168
+ </div>
169
+ }
170
+ affected={deleting?.contact_count ?? undefined}
171
+ confirmLabel="Delete tag"
172
+ confirming={remove.loading}
173
+ onConfirm={() => deleting && void remove.mutate({ id: deleting.id, reassignTo: moveTo })}
174
+ onCancel={() => setDeleting(null)}
175
+ />
176
+ </FormSheet>
177
+ );
178
+ }
@@ -37,8 +37,9 @@ import { RecordLink } from "@/g8/components/screen/record-link";
37
37
  import { plural } from "@/g8/lib/plural";
38
38
  import { useG8Query } from "@/g8/hooks/use-g8-query";
39
39
  import { useUrlState } from "@/g8/hooks/use-url-state";
40
+ import { useUrlFilters } from "@/g8/hooks/use-url-filters";
41
+ import { MoreFilters, TextFilter } from "@/g8/components/screen/filter-controls";
40
42
  import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
41
- import { useDebouncedValue } from "../hooks/use-debounced";
42
43
  import { usePageQuery } from "../hooks/use-page-query";
43
44
  import { errorStatus } from "../lib/crm-types";
44
45
  import { Pager, formatDay, useBulkDelete, useRowSelection } from "../lib/screen-parts";
@@ -54,6 +55,8 @@ export interface WorkspaceTask extends TaskItem {
54
55
  }
55
56
 
56
57
  export interface TasksScreenProps {
58
+ /** The signed-in person's user id. With it the toolbar gets a "My tasks" toggle (assignee filter); without it the toggle is hidden. */
59
+ currentUserId?: string;
57
60
  pageSize?: number;
58
61
  /** Which tab opens first. Default "open". */
59
62
  initialStatus?: "open" | "completed" | "all";
@@ -85,6 +88,10 @@ function PriorityPill({ priority }: { priority?: number | null }) {
85
88
  return <StatusPill tone={priority === 1 ? "negative" : priority === 2 ? "warning" : "neutral"}>{PRIORITY[priority] ?? "Low"}</StatusPill>;
86
89
  }
87
90
 
91
+ /** Every filter the tasks API takes besides the status tab: search, priority, type, linked record type, assignee, creator, source meeting; `mine` = assigned to the signed-in person. */
92
+ const FILTER_KEYS = ["search", "priority", "task_type", "entity_type", "assignee_id", "created_by", "source_meeting_id", "mine"] as const;
93
+ type FilterKey = (typeof FILTER_KEYS)[number];
94
+
88
95
  export function TasksScreen(props: TasksScreenProps) {
89
96
  const { pageSize = 25 } = props;
90
97
  const [idParam, setIdParamRaw] = useUrlState("id");
@@ -94,28 +101,35 @@ export function TasksScreen(props: TasksScreenProps) {
94
101
  const setIdParam = (v: string | null): void => { if (v == null) setView(null); setIdParamRaw(v); };
95
102
  const openId = idParam || null;
96
103
  const [tab, setTab] = useState<Tab>(props.initialStatus ?? "open");
97
- const [search, setSearch] = useState("");
98
- const [priority, setPriority] = useState("");
99
- const [taskType, setTaskType] = useState("");
100
- const [entityType, setEntityType] = useState(props.defaultRecord?.entity_type ?? "");
101
- const q = useDebouncedValue(search);
104
+ // Filters live in the URL (`t_priority=1`, `t_mine=1`), so a filtered view survives reload, Back and sharing.
105
+ const urlFilters = useUrlFilters(FILTER_KEYS, { prefix: "t_" });
106
+ const { draft, filters } = urlFilters;
107
+ const search = draft.search;
108
+ const priority = draft.priority;
109
+ const taskType = draft.task_type;
110
+ const entityType = props.defaultRecord?.entity_type ?? draft.entity_type;
111
+ const q = filters.search;
112
+ // "My tasks" needs the signed-in person's user id: the app passes it as `currentUserId` (the pickers block's
113
+ // useCurrentMember gives it), so this screen holds no team-members scope of its own.
114
+ const assigneeId = draft.mine && props.currentUserId ? props.currentUserId : filters.assignee_id || undefined;
102
115
  const [sheet, setSheet] = useState<"new" | "edit" | null>(null);
103
116
  const [confirmDelete, setConfirmDelete] = useState(false);
104
117
  const [note, setNote] = useState<string | null>(null);
105
118
 
106
- const scope = { status: tab === "all" ? undefined : tab, search: q || undefined, priority: priority || undefined, task_type: taskType || undefined, entity_type: entityType || undefined, entity_id: props.defaultRecord?.entity_id };
119
+ const scope = { status: tab === "all" ? undefined : tab, search: q || undefined, priority: filters.priority || undefined, task_type: filters.task_type || undefined, entity_type: (props.defaultRecord?.entity_type ?? filters.entity_type) || undefined, assignee_id: assigneeId, created_by: filters.created_by || undefined, source_meeting_id: filters.source_meeting_id || undefined, entity_id: props.defaultRecord?.entity_id };
107
120
  const list = usePageQuery<WorkspaceTask>(
108
121
  (page, signal) => g8Get<WorkspaceTask[]>("/tasks", { limit: pageSize, offset: (page - 1) * pageSize, ...scope }, { signal }),
109
- [scope.status, scope.search, scope.priority, scope.task_type, scope.entity_type, scope.entity_id],
122
+ [scope.status, scope.search, scope.priority, scope.task_type, scope.entity_type, scope.entity_id, scope.assignee_id, scope.created_by, scope.source_meeting_id],
110
123
  pageSize,
111
124
  ["tasks", "list", "screen"],
125
+ urlFilters.ready,
112
126
  );
113
127
  const recordScope = { entity_type: props.defaultRecord?.entity_type, entity_id: props.defaultRecord?.entity_id };
114
128
  const openCount = useG8Query((signal) => g8Get<WorkspaceTask[]>("/tasks", { status: "open", limit: 1, ...recordScope }, { signal }), [recordScope.entity_type, recordScope.entity_id], { key: ["tasks", "count", "open", recordScope] });
115
129
  const doneCount = useG8Query((signal) => g8Get<WorkspaceTask[]>("/tasks", { status: "completed", limit: 1, ...recordScope }, { signal }), [recordScope.entity_type, recordScope.entity_id], { key: ["tasks", "count", "completed", recordScope] });
116
130
  const selection = useRowSelection<WorkspaceTask>(JSON.stringify(scope));
117
131
  const reload = () => { void list.refetch(); void openCount.refetch(); void doneCount.refetch(); };
118
- const hasFilters = Boolean(search || priority || taskType || entityType);
132
+ const hasFilters = Boolean(search || priority || taskType || draft.entity_type || draft.assignee_id || draft.created_by || draft.source_meeting_id || draft.mine);
119
133
  const overdue = list.rows.filter(isOverdue).length;
120
134
 
121
135
  const toggle = useG8Mutation((t: WorkspaceTask) => g8Patch(`/tasks/${t.id}`, { status: t.status === "completed" ? "open" : "completed" }), { invalidates: ["tasks"], onSuccess: reload });
@@ -130,7 +144,7 @@ export function TasksScreen(props: TasksScreenProps) {
130
144
  if (openId && gone.includes(openId)) setIdParam(null);
131
145
  reload();
132
146
  });
133
- useEffect(() => { setNote(null); }, [tab, q, priority, taskType, entityType]);
147
+ useEffect(() => { setNote(null); }, [tab, filters]);
134
148
  const allOnPage = list.rows.length > 0 && list.rows.every((r) => selection.selected.has(r.id));
135
149
 
136
150
  return (
@@ -162,21 +176,27 @@ export function TasksScreen(props: TasksScreenProps) {
162
176
  list={
163
177
  <div className="flex flex-col gap-3">
164
178
  <Toolbar
165
- search={{ value: search, onChange: setSearch, placeholder: "Search tasks by title" }}
179
+ search={{ value: search, onChange: (v) => urlFilters.set("search", v), debounceMs: 0, placeholder: "Search tasks by title" }}
166
180
  filters={
167
181
  <>
168
- <select aria-label="Filter by priority" className="h-9 rounded-md border bg-background px-2 text-sm" value={priority} onChange={(e) => setPriority(e.target.value)}>
182
+ <select aria-label="Filter by priority" className="h-9 rounded-md border bg-background px-2 text-sm" value={priority} onChange={(e) => urlFilters.set("priority", e.target.value)}>
169
183
  <option value="">Any priority</option><option value="1">High</option><option value="2">Medium</option><option value="3">Low</option>
170
184
  </select>
171
- <select aria-label="Filter by type" className="h-9 rounded-md border bg-background px-2 text-sm" value={taskType} onChange={(e) => setTaskType(e.target.value)}>
185
+ <select aria-label="Filter by type" className="h-9 rounded-md border bg-background px-2 text-sm" value={taskType} onChange={(e) => urlFilters.set("task_type", e.target.value)}>
172
186
  <option value="">Any type</option>{TASK_TYPES.map((t) => <option key={t} value={t}>{label(t)}</option>)}
173
187
  </select>
174
188
  {props.defaultRecord ? null : (
175
- <select aria-label="Filter by linked record" className="h-9 rounded-md border bg-background px-2 text-sm" value={entityType} onChange={(e) => setEntityType(e.target.value)}>
189
+ <select aria-label="Filter by linked record" className="h-9 rounded-md border bg-background px-2 text-sm" value={entityType} onChange={(e) => urlFilters.set("entity_type", e.target.value)}>
176
190
  <option value="">Any record</option>{ENTITY_TYPES.map((t) => <option key={t} value={t}>{label(t)}</option>)}
177
191
  </select>
178
192
  )}
179
- {hasFilters ? <Button variant="ghost" size="sm" onClick={() => { setSearch(""); setPriority(""); setTaskType(""); setEntityType(props.defaultRecord?.entity_type ?? ""); }}>Clear filters</Button> : null}
193
+ {props.currentUserId ? <Button variant={draft.mine ? "default" : "outline"} size="sm" aria-pressed={Boolean(draft.mine)} onClick={() => urlFilters.set("mine", draft.mine ? "" : "1")}>My tasks</Button> : null}
194
+ <MoreFilters activeCount={["assignee_id", "created_by", "source_meeting_id"].filter((k) => draft[k as FilterKey]).length}>
195
+ <TextFilter label="assignee ID" placeholder="Assignee user ID" value={draft.assignee_id} onChange={(v) => urlFilters.set("assignee_id", v)} className="w-48" />
196
+ <TextFilter label="creator ID" placeholder="Created by user ID" value={draft.created_by} onChange={(v) => urlFilters.set("created_by", v)} className="w-48" />
197
+ <TextFilter label="meeting ID" placeholder="Source meeting ID" value={draft.source_meeting_id} onChange={(v) => urlFilters.set("source_meeting_id", v)} className="w-48" />
198
+ </MoreFilters>
199
+ {hasFilters ? <Button variant="ghost" size="sm" onClick={urlFilters.clear}>Clear filters</Button> : null}
180
200
  </>
181
201
  }
182
202
  />
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ /**
4
+ * UnlockInfoButton: unlock the locked contacts of a selection. Counts what it will charge for, shows that count and
5
+ * cost in ConfirmSpendDialog, and sends exactly that set.
6
+ *
7
+ * SDK operations, behind ConfirmSpendDialog (billable, scope enrichment:run):
8
+ * 1. unlockContactInfo POST /contacts/unlock-info { contact_ids } 1 credit per newly unlocked contact
9
+ * Only locked contacts are counted and sent (a masked email or phone). The answer's `credits_charged` is shown.
10
+ * At most 1000 per call, so a larger selection unlocks the first 1000 and says so before the confirm.
11
+ *
12
+ * The confirm is disabled while its request runs, and a failed call keeps the selection so the person can retry.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button } from "@graph8/sdk/ui";
17
+ import { Unlock } from "lucide-react";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { g8Post } from "@/g8/lib/g8-client";
22
+ import { plural } from "@/g8/lib/plural";
23
+ import type { ContactRecord } from "../lib/crm-types";
24
+ import { MAX_PAID_BATCH, UNLOCK_CREDITS_PER_CONTACT, isLockedContact, unlockTargets } from "../lib/record-actions";
25
+
26
+ export interface PaidActionProps {
27
+ rows: ReadonlyArray<ContactRecord>;
28
+ onDone?: () => void;
29
+ variant?: "default" | "outline" | "secondary" | "ghost";
30
+ size?: "default" | "sm";
31
+ className?: string;
32
+ /** Start with the confirm open and render no trigger (the row actions dialog opens it). */
33
+ defaultOpen?: boolean;
34
+ onClosed?: () => void;
35
+ }
36
+
37
+ export function UnlockInfoButton({ rows, onDone, variant = "outline", size = "sm", className, defaultOpen = false, onClosed }: PaidActionProps) {
38
+ const [open, setOpen] = useState(defaultOpen);
39
+ const [charged, setCharged] = useState<number | null | undefined>(undefined);
40
+ const targets = unlockTargets(rows);
41
+ const lockedTotal = rows.filter(isLockedContact).length;
42
+ const already = rows.length - lockedTotal;
43
+
44
+ const unlock = useG8Mutation(async (ids: number[]) => {
45
+ const res = await g8Post<{ credits_charged?: number }>("/contacts/unlock-info", { contact_ids: ids });
46
+ return typeof res.data?.credits_charged === "number" ? res.data.credits_charged : null;
47
+ }, {
48
+ invalidates: [["contacts", "list"], ["contacts", "detail"], ["contacts", "record"]],
49
+ onSuccess: (n) => { setOpen(false); setCharged(n); onDone?.(); onClosed?.(); },
50
+ });
51
+
52
+ return (
53
+ <>
54
+ {defaultOpen ? null : (
55
+ <Button className={className} variant={variant} size={size} disabled={targets.length === 0} title={targets.length === 0 ? "None of the selected contacts is locked" : undefined} onClick={() => { setCharged(undefined); unlock.reset(); setOpen(true); }}>
56
+ <Unlock className="size-4" aria-hidden />Unlock info
57
+ </Button>
58
+ )}
59
+ {charged !== undefined ? <span role="status" className="text-xs text-muted-foreground">{charged == null ? "Unlocked." : `Unlocked. ${charged.toLocaleString()} ${charged === 1 ? "credit" : "credits"} charged.`}</span> : null}
60
+ {unlock.error && !open ? <ApiError compact error={unlock.error} title="Could not unlock the contacts" /> : null}
61
+ <ConfirmSpendDialog
62
+ open={open}
63
+ tier="billable"
64
+ title={`Unlock ${plural(targets.length, "contact")}`}
65
+ description={
66
+ <>
67
+ Reveals the withheld email and phone numbers. Each newly unlocked contact costs {UNLOCK_CREDITS_PER_CONTACT} credit; contacts that are already unlocked are not charged again.
68
+ {already > 0 ? ` ${plural(already, "selected contact")} ${already === 1 ? "is" : "are"} already unlocked and left out.` : ""}
69
+ {lockedTotal > MAX_PAID_BATCH ? ` Only the first ${MAX_PAID_BATCH.toLocaleString()} locked contacts are unlocked in one go.` : ""}
70
+ {unlock.error ? <span role="alert" className="mt-2 block text-destructive">The unlock failed. You can try again.</span> : null}
71
+ </>
72
+ }
73
+ estimatedCredits={targets.length * UNLOCK_CREDITS_PER_CONTACT}
74
+ affected={targets.length}
75
+ confirmLabel="Unlock"
76
+ confirming={unlock.loading}
77
+ onConfirm={() => void unlock.mutate(targets.map((t) => t.id))}
78
+ onCancel={() => { setOpen(false); onClosed?.(); }}
79
+ />
80
+ </>
81
+ );
82
+ }