@graph8/sdk 0.373.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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -18,7 +18,7 @@ deep-linkable (`?tab=campaigns|profile|personas`, `?id=` the record, `?dtab=` th
18
18
  - **Company profile tab**: the profile plus ICPs; `IcpDetail` shows every field and edits, pins or archives.
19
19
  - **Personas tab**: `PersonaDetail` shows every field, the campaigns that target it, and edits, pins or archives.
20
20
 
21
- Slot props: `toolbarActions`, `renderDetailActions(campaign)`, `extraDetailTabs`, `websiteUrl`. Prerequisites: a
21
+ Slot props: `toolbarActions`, `renderDetailActions(campaign)`, `extraDetailTabs`, `websiteUrl`. `CampaignLinksTab` (`components/campaign-links.tsx`) is a ready `extraDetailTabs` entry: the campaign's landing pages, workflows and newsletters, each opening in your app (`onOpenLandingPage`, `onOpenWorkflow`, `onOpenNewsletter`), with link and unlink for landing pages and newsletters and unlink only for workflows (the API has no link operation for them; a newsletter that belongs to another campaign is not offered, because linking moves it). Prerequisites: a
22
22
  company profile with the real website, an active mailbox, a contact list, and credits for AI generation.
23
23
  `IcpEditor` and `PersonaEditor` gained optional `onOpen` and `refreshKey`; `CampaignAssets` optional
24
24
  `documentId` and `onDocumentChange`.
@@ -42,16 +42,25 @@
42
42
  "get_persona_personas__persona_id__get",
43
43
  "get_safe_to_send_deliverability_safe_to_send_get",
44
44
  "launch_campaign_campaigns__campaign_id__launch_post",
45
+ "link_landing_page_to_campaign_campaigns__campaign_id__landing_pages_post",
46
+ "link_newsletter_campaign_newsletters__newsletter_id__campaign_put",
45
47
  "list_campaign_documents_campaigns__campaign_id__documents_get",
48
+ "list_campaign_landing_pages_campaigns__campaign_id__landing_pages_get",
49
+ "list_campaign_workflows_campaigns__campaign_id__workflows_get",
46
50
  "list_campaigns_campaigns_get",
47
51
  "list_campaigns_for_picker_campaigns_picker_get",
48
52
  "list_icps_icps_get",
53
+ "list_landing_pages_landing_pages_get",
49
54
  "list_mailboxes_mailboxes_get",
55
+ "list_newsletters_newsletters_get",
50
56
  "list_personas_personas_get",
51
57
  "pin_icp_icps__icp_id__pin_post",
52
58
  "pin_persona_personas__persona_id__pin_post",
53
59
  "recommend_campaign_personas_campaigns_recommend_personas_post",
54
60
  "regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post",
61
+ "unlink_campaign_workflow_campaigns__campaign_id__workflows__link_id__delete",
62
+ "unlink_landing_page_from_campaign_campaigns__campaign_id__landing_pages__landing_page_id__delete",
63
+ "unlink_newsletter_campaign_newsletters__newsletter_id__campaign_delete",
55
64
  "unpin_icp_icps__icp_id__unpin_post",
56
65
  "unpin_persona_personas__persona_id__unpin_post",
57
66
  "update_campaign_campaigns__campaign_id__patch",
@@ -307,6 +316,27 @@
307
316
  "write",
308
317
  "destructive"
309
318
  ]
319
+ },
320
+ {
321
+ "name": "CampaignLinksTab",
322
+ "file": "components/campaign-links.tsx",
323
+ "purpose": "The landing pages, workflows and newsletters tied to one campaign: each row opens the record in your app, landing pages and newsletters can be linked and unlinked (workflows only unlinked, the API has no link operation). Use as an extraDetailTabs entry of CampaignsScreen.",
324
+ "operations": [
325
+ "list_campaign_landing_pages_campaigns__campaign_id__landing_pages_get",
326
+ "link_landing_page_to_campaign_campaigns__campaign_id__landing_pages_post",
327
+ "unlink_landing_page_from_campaign_campaigns__campaign_id__landing_pages__landing_page_id__delete",
328
+ "list_landing_pages_landing_pages_get",
329
+ "list_campaign_workflows_campaigns__campaign_id__workflows_get",
330
+ "unlink_campaign_workflow_campaigns__campaign_id__workflows__link_id__delete",
331
+ "list_newsletters_newsletters_get",
332
+ "link_newsletter_campaign_newsletters__newsletter_id__campaign_put",
333
+ "unlink_newsletter_campaign_newsletters__newsletter_id__campaign_delete"
334
+ ],
335
+ "tiers": [
336
+ "read",
337
+ "write",
338
+ "destructive"
339
+ ]
310
340
  }
311
341
  ],
312
342
  "hooks": [
@@ -378,7 +408,10 @@
378
408
  "campaigns:launch",
379
409
  "campaigns:read",
380
410
  "campaigns:run",
381
- "campaigns:write"
411
+ "campaigns:write",
412
+ "newsletter:delete",
413
+ "newsletter:read",
414
+ "newsletter:write"
382
415
  ],
383
416
  "defaults": [
384
417
  {
@@ -0,0 +1,180 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CampaignLinksTab: the landing pages, workflows and newsletters tied to ONE campaign. Each row opens the record in
5
+ * your app (`onOpen*` callbacks route to your own screens), and a landing page or newsletter can be linked or unlinked
6
+ * here. Use it as an `extraDetailTabs` entry of CampaignsScreen:
7
+ * { id: "links", label: "Links", render: (c) => <CampaignLinksTab campaignId={c.id} onOpenLandingPage={(id) => router.push(`/pages?id=${id}`)} /> }
8
+ *
9
+ * SDK operations, in order:
10
+ * 1. listCampaignLandingPages GET /campaigns/{id}/landing-pages (read)
11
+ * listLandingPages GET /landing-pages?limit&offset (read; the "link a page" choices, paged)
12
+ * linkLandingPageToCampaign POST /campaigns/{id}/landing-pages (write)
13
+ * unlinkLandingPageFromCampaign DELETE /campaigns/{id}/landing-pages/{page} (DESTRUCTIVE tier -> ConfirmSpendDialog; the page itself is kept)
14
+ * 2. listCampaignWorkflows GET /campaigns/{id}/workflows (read)
15
+ * unlinkCampaignWorkflow DELETE /campaigns/{id}/workflows/{link_id} (DESTRUCTIVE tier -> ConfirmSpendDialog; the workflow itself is kept)
16
+ * 3. listNewsletters GET /newsletters (read; each row names its campaign_id)
17
+ * linkNewsletterCampaign PUT /newsletters/{id}/campaign (write; a newsletter has ONE campaign)
18
+ * unlinkNewsletterCampaign DELETE /newsletters/{id}/campaign (DESTRUCTIVE tier -> ConfirmSpendDialog; the newsletter is kept)
19
+ *
20
+ * API gaps, stated on screen: there is no operation that links a workflow to a campaign (only list, edit and unlink),
21
+ * and a newsletter that already belongs to ANOTHER campaign is not offered, because linking replaces its campaign.
22
+ */
23
+
24
+ import { useState } from "react";
25
+ import { Button, Skeleton } from "@graph8/sdk/ui";
26
+ import { ExternalLink, Link2, Unlink } from "lucide-react";
27
+ import { ApiError } from "@/g8/components/api-error";
28
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
+ import { EmptyState } from "@/g8/components/empty-state";
30
+ import { SectionCard } from "@/g8/components/screen/section-card";
31
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
32
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
33
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
34
+ import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
35
+ import { hasNextOf } from "@/g8/lib/paging";
36
+ import { text } from "../lib/campaigns-utils";
37
+
38
+ export interface CampaignLinksTabProps {
39
+ /** The campaign (id or slug as the list gave it). */
40
+ campaignId: string;
41
+ /** Route to a landing page / workflow / newsletter in your app. A section without its callback shows names only. */
42
+ onOpenLandingPage?: (landingPageId: string) => void;
43
+ onOpenWorkflow?: (workflowId: string) => void;
44
+ onOpenNewsletter?: (newsletterId: string) => void;
45
+ className?: string;
46
+ }
47
+
48
+ type Row = Record<string, unknown>;
49
+ const PAGE = 100;
50
+
51
+ /** The rows of a list payload under any of the keys the API mirrors them on. */
52
+ export function rowsOf(env: unknown, ...keys: string[]): Row[] {
53
+ const data = (env as { data?: unknown } | null)?.data;
54
+ if (Array.isArray(data)) return data as Row[];
55
+ const o = (data ?? {}) as Record<string, unknown>;
56
+ for (const k of ["items", ...keys]) if (Array.isArray(o[k])) return o[k] as Row[];
57
+ return [];
58
+ }
59
+
60
+ const idOf = (v: unknown): string => text(v).trim();
61
+
62
+ function LinkRow({ name, meta, onOpen, onUnlink, busy }: { name: string; meta?: string; onOpen?: () => void; onUnlink?: () => void; busy?: boolean }) {
63
+ return (
64
+ <li className="flex items-center justify-between gap-3 py-2 text-sm">
65
+ <span className="min-w-0 truncate"><span className="font-medium">{name}</span>{meta ? <span className="text-muted-foreground"> · {meta}</span> : null}</span>
66
+ <span className="flex shrink-0 gap-2">
67
+ {onOpen ? <Button size="sm" variant="outline" onClick={onOpen} aria-label={`Open ${name}`}><ExternalLink className="mr-1 size-4" aria-hidden />Open</Button> : null}
68
+ {onUnlink ? <Button size="sm" variant="ghost" disabled={busy} onClick={onUnlink} aria-label={`Unlink ${name}`}><Unlink className="mr-1 size-4" aria-hidden />Unlink</Button> : null}
69
+ </span>
70
+ </li>
71
+ );
72
+ }
73
+
74
+ function LinkPicker({ label, options, busy, onLink }: { label: string; options: Array<{ id: string; name: string }>; busy: boolean; onLink: (id: string) => void }) {
75
+ const [value, setValue] = useState("");
76
+ return (
77
+ <div className="flex items-center gap-2 pt-2">
78
+ <select aria-label={label} className="h-9 min-w-56 rounded-md border bg-background px-2 text-sm" value={value} onChange={(e) => setValue(e.target.value)}>
79
+ <option value="">{options.length ? label : "Nothing left to link"}</option>
80
+ {options.map((o) => <option key={o.id} value={o.id}>{o.name}</option>)}
81
+ </select>
82
+ <Button size="sm" disabled={!value || busy} loading={busy} onClick={() => { onLink(value); setValue(""); }}><Link2 className="mr-1 size-4" aria-hidden />Link</Button>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ type Pending = { kind: "page" | "workflow" | "newsletter"; id: string; name: string };
88
+
89
+ export function CampaignLinksTab({ campaignId, onOpenLandingPage, onOpenWorkflow, onOpenNewsletter, className }: CampaignLinksTabProps) {
90
+ const base = `/campaigns/${encodeURIComponent(campaignId)}`;
91
+ const pages = useG8Query(async (signal) => rowsOf(await g8Get(`${base}/landing-pages`, undefined, { signal }), "landing_pages"), [campaignId], { key: ["campaigns", "links", "pages", campaignId] });
92
+ const flows = useG8Query(async (signal) => rowsOf(await g8Get(`${base}/workflows`, undefined, { signal }), "workflows"), [campaignId], { key: ["campaigns", "links", "workflows", campaignId] });
93
+ const letters = useG8Query(async (signal) => rowsOf(await g8Get("/newsletters", undefined, { signal }), "newsletters"), [campaignId], { key: ["campaigns", "links", "newsletters"] });
94
+ const allPages = useAllPages<Row>(async (page, signal) => {
95
+ const env = await g8Get("/landing-pages", { limit: PAGE, offset: (page - 1) * PAGE }, { signal });
96
+ const rows = rowsOf(env, "pages", "landing_pages");
97
+ return { rows, hasNext: hasNextOf(env, PAGE, rows.length) };
98
+ }, [], { pageSize: PAGE });
99
+ const [pending, setPending] = useState<Pending | null>(null);
100
+ const refresh = () => { void pages.refetch(); void flows.refetch(); void letters.refetch(); };
101
+ const link = useG8Mutation(async (v: { kind: "page" | "newsletter"; id: string }) => {
102
+ if (v.kind === "page") await g8Post(`${base}/landing-pages`, { landing_page_id: v.id });
103
+ else await g8Put(`/newsletters/${encodeURIComponent(v.id)}/campaign`, { campaign_id: campaignId });
104
+ }, { onSuccess: refresh });
105
+ const unlink = useG8Mutation(async (p: Pending) => {
106
+ try {
107
+ if (p.kind === "page") await g8Delete(`${base}/landing-pages/${encodeURIComponent(p.id)}`);
108
+ else if (p.kind === "workflow") await g8Delete(`${base}/workflows/${encodeURIComponent(p.id)}`);
109
+ else await g8Delete(`/newsletters/${encodeURIComponent(p.id)}/campaign`);
110
+ } catch (err) {
111
+ if ((err as { status?: number }).status !== 404) throw err; // already unlinked counts as done
112
+ }
113
+ }, { onSuccess: () => { setPending(null); refresh(); }, onError: () => setPending(null) });
114
+
115
+ const linkedPages = (pages.data ?? []).map((r) => {
116
+ const lp = (r.landing_page ?? {}) as Row;
117
+ return { id: idOf(lp.id) || idOf(r.landing_page_id), name: text(lp.name) || `Landing page ${idOf(lp.id) || idOf(r.landing_page_id)}`, status: text(lp.status) };
118
+ }).filter((p) => p.id);
119
+ const linkedFlows = (flows.data ?? []).map((r) => ({ linkId: idOf(r.id), workflowId: idOf(r.workflow_id), name: text(r.workflow_name) || `Workflow ${idOf(r.workflow_id)}`, status: text(r.status), role: text(r.role) })).filter((w) => w.linkId);
120
+ const thisLetters = (letters.data ?? []).filter((n) => idOf(n.campaign_id) === campaignId);
121
+ const freeLetters = (letters.data ?? []).filter((n) => !idOf(n.campaign_id)).map((n) => ({ id: idOf(n.id), name: text(n.name) || `Newsletter ${idOf(n.id)}` }));
122
+ const linkedIds = new Set(linkedPages.map((p) => p.id));
123
+ const pageChoices = allPages.items.map((r) => ({ id: idOf(r.id), name: text(r.name) || `Landing page ${idOf(r.id)}` })).filter((p) => p.id && !linkedIds.has(p.id));
124
+ const otherLetters = (letters.data ?? []).filter((n) => idOf(n.campaign_id) && idOf(n.campaign_id) !== campaignId).length;
125
+
126
+ return (
127
+ <div className={`space-y-4 ${className ?? ""}`}>
128
+ <SectionCard title="Landing pages" description="Pages visitors land on from this campaign. Unlinking keeps the page.">
129
+ {pages.error ? <ApiError error={pages.error} title="Could not load landing pages" onRetry={() => void pages.refetch()} /> : !pages.data ? <Skeleton className="h-16 w-full" aria-label="Loading landing pages" /> : linkedPages.length === 0 ? (
130
+ <EmptyState compact title="No landing page linked" description="Link a page below." />
131
+ ) : (
132
+ <ul className="divide-y" aria-label="Linked landing pages">
133
+ {linkedPages.map((p) => <LinkRow key={p.id} name={p.name} meta={p.status || undefined} onOpen={onOpenLandingPage ? () => onOpenLandingPage(p.id) : undefined} onUnlink={() => setPending({ kind: "page", id: p.id, name: p.name })} busy={unlink.loading} />)}
134
+ </ul>
135
+ )}
136
+ {allPages.error ? <ApiError error={allPages.error} title="Could not load the pages to link" onRetry={allPages.refetch} /> : allPages.loaded ? <LinkPicker label="Link a landing page" options={pageChoices} busy={link.loading} onLink={(id) => void link.mutate({ kind: "page", id })} /> : null}
137
+ {allPages.truncated ? <p className="pt-1 text-xs text-muted-foreground">Showing the first {allPages.items.length} landing pages.</p> : null}
138
+ </SectionCard>
139
+
140
+ <SectionCard title="Workflows" description="Automations that run for this campaign. Unlinking keeps the workflow.">
141
+ {flows.error ? <ApiError error={flows.error} title="Could not load workflows" onRetry={() => void flows.refetch()} /> : !flows.data ? <Skeleton className="h-16 w-full" aria-label="Loading workflows" /> : linkedFlows.length === 0 ? (
142
+ <EmptyState compact title="No workflow linked" />
143
+ ) : (
144
+ <ul className="divide-y" aria-label="Linked workflows">
145
+ {linkedFlows.map((w) => <LinkRow key={w.linkId} name={w.name} meta={[w.role, w.status].filter(Boolean).join(", ") || undefined} onOpen={onOpenWorkflow && w.workflowId ? () => onOpenWorkflow(w.workflowId) : undefined} onUnlink={() => setPending({ kind: "workflow", id: w.linkId, name: w.name })} busy={unlink.loading} />)}
146
+ </ul>
147
+ )}
148
+ <p className="pt-2 text-xs text-muted-foreground">Linking a new workflow to a campaign is not available through the API yet.</p>
149
+ </SectionCard>
150
+
151
+ <SectionCard title="Newsletters" description="A newsletter belongs to one campaign. Unlinking keeps the newsletter.">
152
+ {letters.error ? <ApiError error={letters.error} title="Could not load newsletters" onRetry={() => void letters.refetch()} /> : !letters.data ? <Skeleton className="h-16 w-full" aria-label="Loading newsletters" /> : (
153
+ <>
154
+ {thisLetters.length === 0 ? <EmptyState compact title="No newsletter linked" /> : (
155
+ <ul className="divide-y" aria-label="Linked newsletters">
156
+ {thisLetters.map((n) => <LinkRow key={idOf(n.id)} name={text(n.name) || `Newsletter ${idOf(n.id)}`} meta={text(n.status) || undefined} onOpen={onOpenNewsletter ? () => onOpenNewsletter(idOf(n.id)) : undefined} onUnlink={() => setPending({ kind: "newsletter", id: idOf(n.id), name: text(n.name) || `Newsletter ${idOf(n.id)}` })} busy={unlink.loading} />)}
157
+ </ul>
158
+ )}
159
+ <LinkPicker label="Link a newsletter" options={freeLetters} busy={link.loading} onLink={(id) => void link.mutate({ kind: "newsletter", id })} />
160
+ {otherLetters > 0 ? <p className="pt-1 text-xs text-muted-foreground">{otherLetters} {otherLetters === 1 ? "newsletter belongs" : "newsletters belong"} to other campaigns and {otherLetters === 1 ? "is" : "are"} not listed, because linking would move {otherLetters === 1 ? "it" : "them"}.</p> : null}
161
+ </>
162
+ )}
163
+ </SectionCard>
164
+
165
+ {link.error ? <ApiError error={link.error} title="Could not link" onRetry={link.reset} /> : null}
166
+ {unlink.error ? <ApiError error={unlink.error} title="Could not unlink" onRetry={unlink.reset} /> : null}
167
+ <ConfirmSpendDialog
168
+ open={pending !== null}
169
+ tier="destructive"
170
+ title={`Unlink "${pending?.name ?? ""}" from this campaign?`}
171
+ description={pending?.kind === "page" ? "The landing page stays published and keeps its other campaigns. Only the tie to this campaign is removed." : pending?.kind === "workflow" ? "The workflow itself is kept. It stops being part of this campaign." : "The newsletter is kept and keeps sending. It is no longer part of this campaign."}
172
+ confirmLabel="Unlink"
173
+ affected={1}
174
+ confirming={unlink.loading}
175
+ onCancel={() => setPending(null)}
176
+ onConfirm={() => { if (pending) void unlink.mutate(pending); }}
177
+ />
178
+ </div>
179
+ );
180
+ }
@@ -75,8 +75,12 @@ export function ResearchRun({ onChanged, className }: ResearchRunProps) {
75
75
  const sources = runSteps(run ?? undefined).length;
76
76
  const records = docs.data?.length;
77
77
 
78
+ // The confirm's source count comes from the last run's status read: hold Research until that read settled (success or
79
+ // error), so the dialog never opens with a missing count. After an error the confirm says what it collects, no number.
80
+ const countPending = latest.loading || (!activeId && Boolean(shownId) && lastRun.loading);
81
+
78
82
  const askResearch = () => {
79
- if (!website) return;
83
+ if (!website || countPending) return;
80
84
  ask({
81
85
  tier: "billable",
82
86
  title: `Research ${website}`,
@@ -132,7 +136,7 @@ export function ResearchRun({ onChanged, className }: ResearchRunProps) {
132
136
  {target.suggested && !typed ? <button type="button" className="text-xs underline underline-offset-2" onClick={() => setTyped(target.suggested ?? "")}>Use {target.suggested} from your workspace settings</button> : null}
133
137
  </div>
134
138
  )}
135
- <Button type="button" onClick={askResearch} disabled={!website || running}>{running ? "Researching..." : latest.run ? "Research again" : "Research website"}</Button>
139
+ <Button type="button" onClick={askResearch} disabled={!website || running || countPending} title={countPending ? "Loading the last run" : undefined}>{running ? "Researching..." : latest.run ? "Research again" : "Research website"}</Button>
136
140
  {finishedWithinHour(latest.run) ? (
137
141
  <label className="flex items-center gap-1.5 text-sm"><input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />Run again even though one finished in the last hour</label>
138
142
  ) : null}
@@ -18,6 +18,23 @@ Three finished screens compose the parts below into complete workspaces. Each ne
18
18
  | `CompaniesScreen` | Same flow for accounts: filters (name, domain, industry, country), detail with People, Activity, Notes, Tasks, create (domain required), edit, delete, duplicate review. | `renderRowActions(row, { reload })`, `renderDetailActions(company, { reload })`, `extraDetailTabs`, `toolbarActions`, `renderBulkActions`, `onOpenContact` |
19
19
  | `TasksScreen` | The workspace-wide task queue: Open / Completed / All, search, priority / type / linked-record filters, tick to complete or reopen, create, edit, delete, bulk complete and delete, overdue marker. | `renderRowActions`, `renderDetailActions`, `toolbarActions`, `defaultRecord`, `onOpenRecord` |
20
20
 
21
+ ### Record actions on contacts
22
+
23
+ `ContactsScreen` also carries the write actions a CRM needs. Each exists three ways, a row (the "..." button), a selection (the bar
24
+ above the table) and the open contact, and each is an exported component you can place yourself.
25
+
26
+ | Action | Component | Notes |
27
+ |---|---|---|
28
+ | Assign or remove the owner of a WHOLE LIST | `OwnerAssignListButton` (via `ContactsScreen`'s `renderOwnerPicker`) | Never for a selection: the API assigns owners by filter (`list_id` + `filterModel`) across every match, and has no id-addressed owner write (API gap: `assign-owner` needs an `ids` field). The picker is injected (`renderOwnerPicker={(p) => <MemberPicker label="New owner" {...p} />}`) so crm stays free of the pickers block; without it the action is hidden. Offered only with a list in scope and no other filter; N is the list's real count; assign and remove are both confirmed with the list and N. |
29
+ | Tags | `ContactTagChips` (open contact), `TagAssignButton` (selection), `TagManager` (create, rename, recolour, archive, delete, merge) | Delete states how many contacts lose the tag and can move them to another tag first. Merge removes the source tags. A tag's description cannot be cleared (the API ignores null), so it is not editable. |
30
+ | Edit one field on a selection | `BulkUpdateButton` | Title, department, seniority, city, state, country. Chunks of 100, "N of M updated". Clearing is not offered (no documented clear). |
31
+ | Verify email | `VerifyEmailButton` | Billable, about 1 credit per email. Only contacts with an unlocked email are counted and sent; the result line shows the credits actually charged. Max 1000. |
32
+ | Unlock info | `UnlockInfoButton` | Billable, 1 credit per newly unlocked contact. Only locked contacts are counted and sent. Max 1000 per call. |
33
+ | AI documents | `ContactDocumentsTab` | Write, rewrite, write all, read. Billable with no published price, so the confirm shows none. Rewrite replaces the stored text. |
34
+ | Signals and next best step | `ContactInsightsTab` | Signals load on open. The next best step is a billable GET, so it is a button behind a confirm. |
35
+
36
+ Prerequisites added: the `enrichment:run` scope (verify, unlock, documents, next best step). The owner action needs the `team-members` prefix and `team:read` only in apps that inject a picker (they already include `pickers`).
37
+
21
38
  ```tsx
22
39
  "use client";
23
40
  import { ContactsScreen } from "@/components/g8/crm/contacts-screen";
@@ -152,5 +169,7 @@ Ask the person: who may delete or merge records (decide it in the proxy's `autho
152
169
  ## Cross-block glue
153
170
 
154
171
  - `ContactLookupLink` (`components/contact-lookup-link.tsx`): "Open contact" for something that only knows an email (an inbox thread, a meeting attendee). It looks the email up (`GET /contacts?email=`) and shows a button only when a contact has it; `onOpen(contactId)` routes to your contacts screen. `ContactsScreen.onOpenCompany`, `CompaniesScreen.onOpenContact` and `TasksScreen.onOpenRecord` are the other ends of the same links: the stack pages pass them (`router.push("/companies?id=...")`).
172
+ - `ContactLookupLink` can also create: pass `createIfMissing` (optionally with the sender's `firstName`/`lastName`) and a stranger gets "Add to contacts" (`POST /contacts`, then it opens the new contact). Nothing is offered while the lookup loads or after it failed.
173
+ - `RecordOpenLinks` (`components/record-open-links.tsx`): "Open contact" and "Open company" for something that only knows ids (a call log entry, a visitor). The ids are verified with `GET /contacts/{id}` and `GET /companies/{id}` first (optionally against an expected company domain or contact name), so a link never points at a missing or wrong record.
155
174
 
156
175
  - `RecordEngagementTab` (`components/record-engagement-tab.tsx`): engagement counts, last touch (contacts) and pages visited for a contact or company, an `extraDetailTabs` entry: `{ id: "engagement", label: "Engagement", render: (c) => <RecordEngagementTab kind="contacts" id={c.id} /> }`. Reads `getContactEngagementSummary` / `getCompanyEngagementSummary` and `getContactSitesVisited` / `getCompanySitesVisited`. Also: the contact subtitle is `contactSubtitle(contact)` ("VP of Sales at Acme"; the company is never the person's own name, and "at" is left out without a company).
@@ -10,7 +10,12 @@
10
10
  "CSV/Excel import wizard with column mapping and preview",
11
11
  "duplicate review with merge, bulk merge and undo",
12
12
  "locked (masked) emails and phones shown as Locked, never as values",
13
- "finished ContactsScreen, CompaniesScreen and TasksScreen (list, detail, create, edit, delete, import, duplicates)"
13
+ "finished ContactsScreen, CompaniesScreen and TasksScreen (list, detail, create, edit, delete, import, duplicates)",
14
+ "assign owner to all contacts of a list with an injected team-member picker (never for a selection), confirmed with the list and N",
15
+ "contact tags: chips on the contact, add or remove on a selection, and a tag manager (create, rename, recolour, archive, delete, merge)",
16
+ "bulk edit of one field on a selection",
17
+ "verify emails and unlock locked contacts, each confirmed with the real count and cost",
18
+ "AI documents, signals and an on-demand next best step on the open contact"
14
19
  ],
15
20
  "dependsOn": [
16
21
  "foundation"
@@ -20,9 +25,13 @@
20
25
  "assert_company_companies_assert_put",
21
26
  "assert_contact_contacts_assert_put",
22
27
  "assert_contacts_batch_contacts_assert_batch_put",
28
+ "assign_contact_owner_contacts_assign_owner_post",
29
+ "assign_contact_tags_contacts_tags_assign_post",
30
+ "bulk_update_contacts_contacts_bulk_update_post",
23
31
  "create_company_companies_post",
24
32
  "create_company_note_companies__company_id__notes_post",
25
33
  "create_company_task_companies__company_id__tasks_post",
34
+ "create_contact_tag_contacts_tags_post",
26
35
  "create_contacts_contacts_post",
27
36
  "create_field_fields_post",
28
37
  "create_global_task_tasks_post",
@@ -31,17 +40,25 @@
31
40
  "create_task_contacts__contact_id__tasks_post",
32
41
  "delete_company_companies__company_id__delete",
33
42
  "delete_contact_contacts__contact_id__delete",
43
+ "delete_contact_tag_contacts_tags__tag_id__delete",
34
44
  "delete_note_notes__note_id__delete",
35
45
  "delete_task_tasks__task_id__delete",
36
46
  "duplicate_counts_duplicates_counts_get",
47
+ "generate_all_contact_documents_contacts__contact_id__documents_generate_all_post",
48
+ "generate_contact_document_contacts__contact_id__documents_types__doc_type__generate_post",
37
49
  "get_activity_feed_activity_feed_get",
38
50
  "get_bulk_merge_status_duplicates_bulk_merge__run_id__get",
39
51
  "get_company_companies__company_id__get",
40
52
  "get_company_engagement_summary_companies__company_id__engagement_summary_get",
41
53
  "get_company_sites_visited_companies__company_id__sites_visited_get",
42
54
  "get_contact_contacts__contact_id__get",
55
+ "get_contact_document_contacts__contact_id__documents_types__doc_type__get",
43
56
  "get_contact_engagement_summary_contacts__contact_id__engagement_summary_get",
57
+ "get_contact_generation_status_contacts__contact_id__documents_generation_status_get",
58
+ "get_contact_next_best_step_contacts__contact_id__next_best_step_get",
59
+ "get_contact_signals_contacts__contact_id__signals_get",
44
60
  "get_contact_sites_visited_contacts__contact_id__sites_visited_get",
61
+ "get_contact_tag_ids_contacts__contact_id__tags_get",
45
62
  "get_task_tasks__task_id__get",
46
63
  "import_contacts_imports__import_id__contacts_post",
47
64
  "list_all_tasks_tasks_get",
@@ -50,6 +67,7 @@
50
67
  "list_company_notes_companies__company_id__notes_get",
51
68
  "list_company_tasks_companies__company_id__tasks_get",
52
69
  "list_contact_fields_fields_get",
70
+ "list_contact_tags_contacts_tags_get",
53
71
  "list_contact_tasks_contacts__contact_id__tasks_get",
54
72
  "list_contacts_contacts_get",
55
73
  "list_duplicates_duplicates_get",
@@ -58,16 +76,22 @@
58
76
  "list_recent_merges_duplicates_recent_merges_get",
59
77
  "merge_company_companies__company_id__merge_post",
60
78
  "merge_contact_contacts__contact_id__merge_post",
79
+ "merge_contact_tags_contacts_tags_merge_post",
61
80
  "preview_import_file_imports__import_id__preview_post",
81
+ "regenerate_contact_document_contacts__contact_id__documents_types__doc_type__regenerate_post",
62
82
  "reject_suggestion_duplicates__suggestion_id__reject_post",
63
83
  "set_field_values_batch_fields_values_batch_patch",
64
84
  "start_bulk_merge_duplicates_bulk_merge_post",
85
+ "unassign_contact_tags_contacts_tags_unassign_post",
65
86
  "undo_merge_duplicates_undo_merge__merge_history_id__post",
87
+ "unlock_contact_info_contacts_unlock_info_post",
66
88
  "update_company_companies__company_id__patch",
67
89
  "update_contact_contacts__contact_id__patch",
90
+ "update_contact_tag_contacts_tags__tag_id__patch",
68
91
  "update_note_notes__note_id__patch",
69
92
  "update_task_tasks__task_id__patch",
70
- "upload_import_file_imports_post"
93
+ "upload_import_file_imports_post",
94
+ "verify_contact_emails_contacts_verify_email_post"
71
95
  ],
72
96
  "components": [
73
97
  {
@@ -268,15 +292,36 @@
268
292
  {
269
293
  "name": "ContactsScreen",
270
294
  "file": "components/contacts-screen.tsx",
271
- "purpose": "Finished contacts workspace: search, filters, sort, paging, bulk delete, detail panel with activity/notes/tasks, create, edit, CSV import and duplicate review. No required props; slot props for other blocks.",
295
+ "purpose": "Finished contacts workspace: search, filters, sort, paging, owner column, bulk delete, detail panel with activity/notes/tasks/AI documents/insights, create, edit, CSV import and duplicate review, plus record actions: assign owner to a whole list (with an injected picker), tags (chips, bulk, manager), bulk edit, verify email and unlock info. No required props; slot props for other blocks.",
272
296
  "operations": [
297
+ "assign_contact_owner_contacts_assign_owner_post",
298
+ "assign_contact_tags_contacts_tags_assign_post",
299
+ "bulk_update_contacts_contacts_bulk_update_post",
300
+ "create_contact_tag_contacts_tags_post",
273
301
  "delete_contact_contacts__contact_id__delete",
302
+ "delete_contact_tag_contacts_tags__tag_id__delete",
274
303
  "duplicate_counts_duplicates_counts_get",
304
+ "generate_all_contact_documents_contacts__contact_id__documents_generate_all_post",
305
+ "generate_contact_document_contacts__contact_id__documents_types__doc_type__generate_post",
275
306
  "get_contact_contacts__contact_id__get",
276
- "list_contacts_contacts_get"
307
+ "get_contact_document_contacts__contact_id__documents_types__doc_type__get",
308
+ "get_contact_generation_status_contacts__contact_id__documents_generation_status_get",
309
+ "get_contact_next_best_step_contacts__contact_id__next_best_step_get",
310
+ "get_contact_signals_contacts__contact_id__signals_get",
311
+ "get_contact_tag_ids_contacts__contact_id__tags_get",
312
+ "list_contact_tags_contacts_tags_get",
313
+ "list_contacts_contacts_get",
314
+ "merge_contact_tags_contacts_tags_merge_post",
315
+ "regenerate_contact_document_contacts__contact_id__documents_types__doc_type__regenerate_post",
316
+ "unassign_contact_tags_contacts_tags_unassign_post",
317
+ "unlock_contact_info_contacts_unlock_info_post",
318
+ "update_contact_tag_contacts_tags__tag_id__patch",
319
+ "verify_contact_emails_contacts_verify_email_post"
277
320
  ],
278
321
  "tiers": [
279
322
  "read",
323
+ "write",
324
+ "billable",
280
325
  "destructive"
281
326
  ]
282
327
  },
@@ -326,10 +371,24 @@
326
371
  {
327
372
  "name": "ContactLookupLink",
328
373
  "file": "components/contact-lookup-link.tsx",
329
- "purpose": "\"Open contact\" button for something that only knows an email (inbox thread, meeting attendee, visitor): looks the email up in the CRM and calls onOpen(contactId); renders nothing when nobody has it.",
374
+ "purpose": "\"Open contact\" button for something that only knows an email (inbox thread, meeting attendee, visitor, form submission): looks the email up in the CRM and calls onOpen(contactId); renders nothing when nobody has it, or with createIfMissing offers \"Add to contacts\" (creates the contact from the email and opens it).",
330
375
  "operations": [
376
+ "create_contacts_contacts_post",
331
377
  "list_contacts_contacts_get"
332
378
  ],
379
+ "tiers": [
380
+ "read",
381
+ "write"
382
+ ]
383
+ },
384
+ {
385
+ "name": "RecordOpenLinks",
386
+ "file": "components/record-open-links.tsx",
387
+ "purpose": "\"Open contact\" and \"Open company\" buttons for something that only knows ids (a call log entry, a website visitor). The ids are verified against the CRM first (and a company's domain, when given), so a button never points at a missing or wrong record.",
388
+ "operations": [
389
+ "get_company_companies__company_id__get",
390
+ "get_contact_contacts__contact_id__get"
391
+ ],
333
392
  "tiers": [
334
393
  "read"
335
394
  ]
@@ -347,6 +406,133 @@
347
406
  "tiers": [
348
407
  "read"
349
408
  ]
409
+ },
410
+ {
411
+ "name": "OwnerAssignListButton",
412
+ "file": "components/owner-assign.tsx",
413
+ "purpose": "\"Assign owner to all N contacts in this list\" with an injected team-member picker (renderPicker), hidden without one. Never for a selection: the API assigns by filter. Both assign and remove are confirmed with the list and N.",
414
+ "operations": [
415
+ "assign_contact_owner_contacts_assign_owner_post"
416
+ ],
417
+ "tiers": [
418
+ "write"
419
+ ]
420
+ },
421
+ {
422
+ "name": "ContactTagChips",
423
+ "file": "components/contact-tag-chips.tsx",
424
+ "purpose": "The tags on one contact as chips with remove buttons and an Add tag picker that can create a tag.",
425
+ "operations": [
426
+ "assign_contact_tags_contacts_tags_assign_post",
427
+ "create_contact_tag_contacts_tags_post",
428
+ "get_contact_tag_ids_contacts__contact_id__tags_get",
429
+ "list_contact_tags_contacts_tags_get",
430
+ "unassign_contact_tags_contacts_tags_unassign_post"
431
+ ],
432
+ "tiers": [
433
+ "read",
434
+ "write"
435
+ ]
436
+ },
437
+ {
438
+ "name": "TagAssignButton",
439
+ "file": "components/tag-assign.tsx",
440
+ "purpose": "Add tags to, or remove tags from, a selection of contacts.",
441
+ "operations": [
442
+ "assign_contact_tags_contacts_tags_assign_post",
443
+ "list_contact_tags_contacts_tags_get",
444
+ "unassign_contact_tags_contacts_tags_unassign_post"
445
+ ],
446
+ "tiers": [
447
+ "read",
448
+ "write"
449
+ ]
450
+ },
451
+ {
452
+ "name": "TagManager",
453
+ "file": "components/tag-manager.tsx",
454
+ "purpose": "Create, rename, recolour, archive, delete and merge contact tags. Delete and merge are confirmed.",
455
+ "operations": [
456
+ "create_contact_tag_contacts_tags_post",
457
+ "delete_contact_tag_contacts_tags__tag_id__delete",
458
+ "list_contact_tags_contacts_tags_get",
459
+ "merge_contact_tags_contacts_tags_merge_post",
460
+ "update_contact_tag_contacts_tags__tag_id__patch"
461
+ ],
462
+ "tiers": [
463
+ "read",
464
+ "write",
465
+ "destructive"
466
+ ]
467
+ },
468
+ {
469
+ "name": "BulkUpdateButton",
470
+ "file": "components/bulk-update.tsx",
471
+ "purpose": "Set one field (title, department, seniority, city, state, country) on every selected contact.",
472
+ "operations": [
473
+ "bulk_update_contacts_contacts_bulk_update_post"
474
+ ],
475
+ "tiers": [
476
+ "write"
477
+ ]
478
+ },
479
+ {
480
+ "name": "VerifyEmailButton",
481
+ "file": "components/verify-email-button.tsx",
482
+ "purpose": "Verify the work emails of a selection (billable, confirmed with the real count and cost).",
483
+ "operations": [
484
+ "verify_contact_emails_contacts_verify_email_post"
485
+ ],
486
+ "tiers": [
487
+ "billable"
488
+ ]
489
+ },
490
+ {
491
+ "name": "UnlockInfoButton",
492
+ "file": "components/unlock-info-button.tsx",
493
+ "purpose": "Unlock the locked contacts of a selection (billable, confirmed with the locked count and cost).",
494
+ "operations": [
495
+ "unlock_contact_info_contacts_unlock_info_post"
496
+ ],
497
+ "tiers": [
498
+ "billable"
499
+ ]
500
+ },
501
+ {
502
+ "name": "ContactRowActions",
503
+ "file": "components/contact-row-actions.tsx",
504
+ "purpose": "The row's ... button: owner, tags, verify and unlock for one contact.",
505
+ "operations": [],
506
+ "tiers": []
507
+ },
508
+ {
509
+ "name": "ContactDocumentsTab",
510
+ "file": "components/contact-documents-tab.tsx",
511
+ "purpose": "AI documents about one contact: status, write, rewrite, write all (billable, confirmed) and read.",
512
+ "operations": [
513
+ "generate_all_contact_documents_contacts__contact_id__documents_generate_all_post",
514
+ "generate_contact_document_contacts__contact_id__documents_types__doc_type__generate_post",
515
+ "get_contact_document_contacts__contact_id__documents_types__doc_type__get",
516
+ "get_contact_generation_status_contacts__contact_id__documents_generation_status_get",
517
+ "regenerate_contact_document_contacts__contact_id__documents_types__doc_type__regenerate_post"
518
+ ],
519
+ "tiers": [
520
+ "read",
521
+ "billable"
522
+ ]
523
+ },
524
+ {
525
+ "name": "ContactInsightsTab",
526
+ "file": "components/contact-insights-tab.tsx",
527
+ "purpose": "Signals for one contact and an on-demand next best step (billable, confirmed).",
528
+ "operations": [
529
+ "get_contact_next_best_step_contacts__contact_id__next_best_step_get",
530
+ "get_contact_signals_contacts__contact_id__signals_get"
531
+ ],
532
+ "tiers": [
533
+ "read",
534
+ "billable"
535
+ ]
350
536
  }
351
537
  ],
352
538
  "hooks": [
@@ -373,6 +559,14 @@
373
559
  "name": "useAllLists",
374
560
  "file": "hooks/use-all-lists.ts",
375
561
  "purpose": "Every list, page after page, for lookups by id (a list's size or name); ListSelect is the picker."
562
+ },
563
+ {
564
+ "name": "useContactTags",
565
+ "file": "hooks/use-contact-tags.ts",
566
+ "purpose": "The org's contact tags with counts, cached so chips, dialogs and the manager refresh together.",
567
+ "operations": [
568
+ "list_contact_tags_contacts_tags_get"
569
+ ]
376
570
  }
377
571
  ],
378
572
  "lib": [
@@ -387,6 +581,10 @@
387
581
  {
388
582
  "file": "lib/read-lists.ts",
389
583
  "purpose": "One page of audience lists (GET /lists), the read behind ListSelect and useAllLists."
584
+ },
585
+ {
586
+ "file": "lib/record-actions.ts",
587
+ "purpose": "Tag type, the id-set grid filter, the paid-batch cap, unlock and verify target sets and cost constants, bulk-edit fields and the verify summary, shared by the record-action components."
390
588
  }
391
589
  ],
392
590
  "recipes": [
@@ -421,7 +619,7 @@
421
619
  {
422
620
  "id": "api-key-scopes",
423
621
  "where": "api",
424
- "description": "The server API key carries the scopes in this block's `scopes` (contacts, companies, lists and notes/tasks read+write; contacts:delete for bulk merge).",
622
+ "description": "The server API key carries the scopes in this block's `scopes` (contacts, companies, lists and notes/tasks read+write; contacts:delete for bulk merge; enrichment:run for verify email, unlock info, AI documents and next best step, all of which spend credits).",
425
623
  "verify": "recipes/verify-proxy-key.ts passes and g8.api.me.describeCurrentKey() lists the scopes."
426
624
  },
427
625
  {
@@ -438,6 +636,7 @@
438
636
  "contacts:delete",
439
637
  "contacts:read",
440
638
  "contacts:write",
639
+ "enrichment:run",
441
640
  "fields:read",
442
641
  "fields:write",
443
642
  "lists:read",