@graph8/sdk 0.308.1 → 0.357.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 (654) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +1440 -21
  390. package/dist/index.d.ts +1440 -21
  391. package/dist/index.js +42 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +42 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +1340 -18
  400. package/dist/react.d.ts +1340 -18
  401. package/dist/react.js +41 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +41 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +6 -3
  611. package/docs/api/appObjects.d.ts +1 -1
  612. package/docs/api/appObjects.md +1 -1
  613. package/docs/api/by-path.md +29 -0
  614. package/docs/api/objects.d.ts +72 -5
  615. package/docs/api/objects.md +84 -7
  616. package/docs/api/processLists.d.ts +247 -0
  617. package/docs/api/processLists.md +409 -0
  618. package/docs/api/reports.d.ts +476 -0
  619. package/docs/api/reports.md +539 -0
  620. package/docs/api/scheduleBuy.d.ts +12 -8
  621. package/docs/api/scheduleBuy.md +13 -10
  622. package/docs/api/seo.d.ts +406 -3
  623. package/docs/api/seo.md +505 -1
  624. package/docs/api/workspaceBackup.d.ts +324 -0
  625. package/docs/api/workspaceBackup.md +427 -0
  626. package/package.json +174 -5
  627. package/stacks/6sense.json +46 -0
  628. package/stacks/README.md +20 -0
  629. package/stacks/apollo.json +128 -0
  630. package/stacks/clay.json +59 -0
  631. package/stacks/gong.json +72 -0
  632. package/stacks/hubspot.json +162 -0
  633. package/stacks/intercom.json +53 -0
  634. package/stacks/mailchimp.json +71 -0
  635. package/stacks/outreach.json +86 -0
  636. package/stacks/salesforce.json +94 -0
  637. package/stacks/stack.schema.json +32 -0
  638. package/stacks/starter.json +22 -0
  639. package/styles/styles.css +2 -0
  640. package/styles/theme.css +181 -0
  641. package/styles/tokens.css +284 -0
  642. package/templates/next-app/README.md.tmpl +35 -0
  643. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  644. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  645. package/templates/next-app/app/globals.css.tmpl +8 -0
  646. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  647. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  648. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  649. package/templates/next-app/dot-gitignore.tmpl +7 -0
  650. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  651. package/templates/next-app/next.config.mjs.tmpl +4 -0
  652. package/templates/next-app/package.json.tmpl +25 -0
  653. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  654. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,164 @@
1
+ "use client";
2
+
3
+ /**
4
+ * DedupeReview: work through pending duplicate suggestions for contacts or
5
+ * companies. Merge one pair by choosing the record to keep, mark a pair as "not
6
+ * duplicates", merge many with a rule (polled to completion), and undo recent merges.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. duplicateCounts GET /duplicates/counts?entity_type (read) badge
10
+ * 2. listDuplicates GET /duplicates?entity_type&status=pending&cursor (read) pairs, cursor-paginated
11
+ * 3. rejectSuggestion POST /duplicates/{suggestion_id}/reject (write) "not duplicates"
12
+ * 4. mergeContact | mergeCompany POST /contacts/{keep}/merge | /companies/{keep}/merge (destructive, ConfirmSpendDialog)
13
+ * 5. startBulkMerge POST /duplicates/bulk-merge -> run_id (write, needs contacts:delete) behind ConfirmSpendDialog
14
+ * getBulkMergeStatus GET /duplicates/bulk-merge/{run_id} (read) polled with useJobPoll until `completed`
15
+ * 6. listRecentMerges GET /duplicates/recent-merges (read)
16
+ * undoMerge POST /duplicates/undo-merge/{id} (write) only rows with can_undo
17
+ *
18
+ * A merge is destructive: the record that is not kept stops existing (undo is possible for 30 days).
19
+ */
20
+
21
+ import { useState } from "react";
22
+ import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { EmptyState } from "@/g8/components/empty-state";
26
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
27
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
28
+ import { useJobPoll } from "@/g8/hooks/use-job-poll";
29
+ import { usePaginated } from "@/g8/hooks/use-paginated";
30
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
31
+ import { isMasked } from "../lib/crm-types";
32
+
33
+ export interface DedupeReviewProps {
34
+ entity: "contact" | "company";
35
+ /** Called after a merge or undo, so tables can reload. */
36
+ onChanged?: () => void;
37
+ className?: string;
38
+ }
39
+
40
+ type Pair = Record<string, string | number | null | undefined> & { suggestion_id: string; a_id: number; b_id: number; confidence?: string };
41
+ interface MergeRow { id: string; primary_name?: string | null; secondary_name?: string | null; merged_at?: string | null; undone_at?: string | null; can_undo: boolean }
42
+ interface BulkRun { status: "running" | "completed"; total: number; merged: number; blocked: number; stale: number }
43
+
44
+ const CRITERIA = [
45
+ ["most_recent_engagement", "Most recent engagement"], ["oldest_engagement", "Oldest engagement"],
46
+ ["created_first", "Created first"], ["created_last", "Created last"], ["most_recently_updated", "Most recently updated"],
47
+ ] as const;
48
+
49
+ function side(pair: Pair, s: "a" | "b", entity: "contact" | "company") {
50
+ const name = entity === "contact" ? [pair[`${s}_first_name`], pair[`${s}_last_name`]].filter(Boolean).join(" ") : String(pair[`${s}_name`] ?? "");
51
+ const lines = entity === "contact" ? [pair[`${s}_email`], pair[`${s}_phone`], pair[`${s}_owner_name`]] : [pair[`${s}_domain`], pair[`${s}_industry`], pair[`${s}_owner_name`]];
52
+ return { id: pair[`${s}_id`] as number, name: name || "Unnamed", lines: lines.filter(Boolean).map((l) => (isMasked(l) ? "Locked" : String(l))) };
53
+ }
54
+
55
+ export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps) {
56
+ const plural = entity === "contact" ? "contacts" : "companies";
57
+ const [merging, setMerging] = useState<{ pair: Pair; keep: "a" | "b" } | null>(null);
58
+ const [picked, setPicked] = useState<Set<string>>(new Set());
59
+ const [criterion, setCriterion] = useState<(typeof CRITERIA)[number][0]>("most_recent_engagement");
60
+ const [bulkOpen, setBulkOpen] = useState(false);
61
+ const [runId, setRunId] = useState<string | null>(null);
62
+
63
+ const counts = useG8Query((signal) => g8Get<{ pending?: number; count?: number }>("/duplicates/counts", { entity_type: entity }, { signal }), [entity]);
64
+ const pairs = usePaginated<Pair>(async (cursor, signal) => {
65
+ const res = await g8Get<{ items: Pair[]; next_cursor?: string | null }>("/duplicates", { entity_type: entity, status: "pending", limit: 25, cursor }, { signal });
66
+ return { data: res.data.items ?? [], pagination: { next_cursor: res.data.next_cursor ?? null } };
67
+ }, [entity]);
68
+ const recent = useG8Query((signal) => g8Get<MergeRow[]>("/duplicates/recent-merges", { entity_type: entity, limit: 25 }, { signal }), [entity]);
69
+ const [busy, setBusy] = useState<string | null>(null);
70
+ const dropPick = (id: string | undefined) => id && setPicked((p) => { const n = new Set(p); n.delete(id); return n; });
71
+ const refreshAll = () => { void pairs.reload(); void counts.refetch(); void recent.refetch(); onChanged?.(); };
72
+
73
+ const merge = useG8Mutation((v: { pair: Pair; keep: "a" | "b" }) => {
74
+ const keepId = v.pair[`${v.keep}_id`], dropId = v.pair[`${v.keep === "a" ? "b" : "a"}_id`];
75
+ return g8Post(`/${plural}/${keepId}/merge`, { secondary_id: dropId, suggestion_id: v.pair.suggestion_id });
76
+ }, { onSuccess: (_r, v) => { dropPick(v.pair.suggestion_id); setMerging(null); refreshAll(); } });
77
+ const reject = useG8Mutation((id: string) => { setBusy(id); return g8Post(`/duplicates/${id}/reject`, {}); }, { onSuccess: (_r, id) => { dropPick(id); setBusy(null); refreshAll(); }, onError: () => setBusy(null) });
78
+ const undo = useG8Mutation((id: string) => { setBusy(id); return g8Post(`/duplicates/undo-merge/${id}`, {}); }, { onSuccess: () => { setBusy(null); refreshAll(); }, onError: () => setBusy(null) });
79
+ const bulk = useG8Mutation(() => g8Post<{ run_id: string }>("/duplicates/bulk-merge", { suggestion_ids: [...picked], criterion }), {
80
+ onSuccess: (r) => { setBulkOpen(false); setRunId(r.data.run_id); },
81
+ });
82
+ const run = useJobPoll<BulkRun>(
83
+ async (signal) => (await g8Get<BulkRun>(`/duplicates/bulk-merge/${runId}`, undefined, { signal })).data,
84
+ { enabled: runId != null, isTerminal: (s) => s.status === "completed", onDone: () => { setPicked(new Set()); setRunId(null); refreshAll(); } },
85
+ );
86
+
87
+ const pending = counts.data?.data.pending ?? counts.data?.data.count;
88
+ const togglePick = (id: string) => setPicked((p) => { const n = new Set(p); if (!n.delete(id)) n.add(id); return n; });
89
+ const mergeError = merge.error ?? reject.error ?? undo.error ?? bulk.error;
90
+
91
+ return (
92
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
93
+ <Tabs defaultValue="pending">
94
+ <TabsList>
95
+ <TabsTrigger value="pending">Suggestions{pending != null ? ` (${pending})` : ""}</TabsTrigger>
96
+ <TabsTrigger value="recent">Recent merges</TabsTrigger>
97
+ </TabsList>
98
+
99
+ <TabsContent value="pending" className="flex flex-col gap-3 pt-3">
100
+ {picked.size > 0 ? (
101
+ <div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm" role="status">
102
+ <span>{picked.size} pairs selected, keep the record with</span>
103
+ <select aria-label="Merge rule" className="h-8 rounded-md border bg-background px-2" value={criterion} onChange={(e) => setCriterion(e.target.value as typeof criterion)}>
104
+ {CRITERIA.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
105
+ </select>
106
+ <Button size="sm" variant="destructive" loading={run.polling} onClick={() => setBulkOpen(true)}>Merge selected</Button>
107
+ </div>
108
+ ) : null}
109
+ {run.polling || run.status ? <p role="status" className="text-sm text-muted-foreground">Merging: {run.status?.merged ?? 0} of {run.status?.total ?? picked.size} done, {run.status?.blocked ?? 0} blocked.</p> : null}
110
+ {mergeError ? <ApiError error={mergeError} title="That did not go through" /> : null}
111
+ {pairs.error ? <ApiError error={pairs.error} title="Could not load duplicates" onRetry={() => void pairs.reload()} /> : null}
112
+ {!pairs.loading && !pairs.error && pairs.items.length === 0 ? <EmptyState title="No duplicates to review" description="New suggestions appear here when graph8 finds likely matches." /> : null}
113
+
114
+ {pairs.items.map((pair) => (
115
+ <div key={pair.suggestion_id} className="rounded-md border p-3" data-testid="duplicate-pair">
116
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-[auto_1fr_1fr]">
117
+ <input type="checkbox" aria-label="Select pair" checked={picked.has(pair.suggestion_id)} onChange={() => togglePick(pair.suggestion_id)} />
118
+ {(["a", "b"] as const).map((s) => {
119
+ const r = side(pair, s, entity);
120
+ return (
121
+ <div key={s}>
122
+ <p className="text-sm font-medium">{r.name}</p>
123
+ {r.lines.map((l) => <p key={l} className="text-xs text-muted-foreground">{l}</p>)}
124
+ <Button className="mt-2" size="sm" variant="outline" onClick={() => setMerging({ pair, keep: s })}>Keep this one</Button>
125
+ </div>
126
+ );
127
+ })}
128
+ </div>
129
+ <div className="mt-2 flex justify-end">
130
+ <Button size="sm" variant="ghost" loading={reject.loading && busy === pair.suggestion_id} onClick={() => void reject.mutate(pair.suggestion_id)}>Not duplicates</Button>
131
+ </div>
132
+ </div>
133
+ ))}
134
+ {pairs.hasMore ? <Button variant="outline" size="sm" loading={pairs.loadingMore} onClick={() => void pairs.loadMore()}>Load more</Button> : null}
135
+ </TabsContent>
136
+
137
+ <TabsContent value="recent" className="flex flex-col gap-2 pt-3">
138
+ {recent.error ? <ApiError error={recent.error} title="Could not load recent merges" onRetry={() => void recent.refetch()} /> : null}
139
+ {(recent.data?.data ?? []).length === 0 && !recent.loading ? <EmptyState title="No merges yet" /> : null}
140
+ {(recent.data?.data ?? []).map((m) => (
141
+ <div key={m.id} className="flex items-center justify-between rounded-md border p-3 text-sm">
142
+ <span>{m.secondary_name ?? "Record"} merged into {m.primary_name ?? "record"}{m.merged_at ? `, ${new Date(m.merged_at).toLocaleDateString()}` : ""}{m.undone_at ? " (undone)" : ""}</span>
143
+ {m.can_undo ? <Button size="sm" variant="outline" loading={undo.loading && busy === m.id} onClick={() => void undo.mutate(m.id)}>Undo</Button> : null}
144
+ </div>
145
+ ))}
146
+ </TabsContent>
147
+ </Tabs>
148
+
149
+ <ConfirmSpendDialog
150
+ open={merging != null}
151
+ tier="destructive"
152
+ title={merging ? `Keep ${side(merging.pair, merging.keep, entity).name}` : ""}
153
+ description={merging ? `${side(merging.pair, merging.keep === "a" ? "b" : "a", entity).name} is merged into it and stops existing. You can undo for 30 days.` : undefined}
154
+ affected={1}
155
+ confirmLabel="Merge"
156
+ confirming={merge.loading}
157
+ onConfirm={() => merging && void merge.mutate(merging)}
158
+ onCancel={() => setMerging(null)}
159
+ />
160
+ <ConfirmSpendDialog open={bulkOpen} tier="destructive" title={`Merge ${picked.size} pairs`} description="The surviving record of each pair is chosen by the rule you picked."
161
+ affected={picked.size} confirmLabel="Merge" confirming={bulk.loading} onConfirm={() => void bulk.mutate(undefined)} onCancel={() => setBulkOpen(false)} />
162
+ </div>
163
+ );
164
+ }
@@ -0,0 +1,193 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ImportWizard: bring a CSV or Excel file into a list in four steps:
5
+ * upload, map columns (with a live preview), choose destination and policy, import.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. uploadImportFile POST /imports (write) multipart `file`; returns datafile_id, row_count, columns
9
+ * 2. previewImportFile POST /imports/{datafile_id}/preview (read) body = the column mapping you intend to import with
10
+ * 3. listLists GET /lists (read) destination picker
11
+ * createList POST /lists (write) optional "new list"
12
+ * 4. importContacts POST /imports/{datafile_id}/contacts (write) list_id, column_mapping, update_policy, enrich_from_graph8
13
+ *
14
+ * The importer drops columns it cannot recognise SILENTLY, so the mapping is always sent
15
+ * explicitly and previewed first. `update_policy` defaults to autofill here (the API default,
16
+ * override, replaces existing values): choosing override, or enriching from graph8, goes
17
+ * through ConfirmSpendDialog before anything is written.
18
+ */
19
+
20
+ import { useEffect, useState } from "react";
21
+ import { Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
27
+
28
+ export interface ImportWizardProps {
29
+ /** Largest file accepted. Default 5 MiB: the /api/g8 proxy rejects bigger bodies (raise maxBodyBytes there first). */
30
+ maxBytes?: number;
31
+ /** Preselect a destination and skip the picker. */
32
+ listId?: number;
33
+ onImported?: (result: ImportResult) => void;
34
+ className?: string;
35
+ }
36
+
37
+ export interface ImportResult { imported?: number | null; updated?: number | null; skipped?: number | null; failed?: number | null; list_id?: number | null }
38
+ interface Upload { datafile_id: string; row_count?: number | null; columns: string[] }
39
+ interface Preview { rows: Array<Record<string, unknown>>; total: number }
40
+ interface ListOption { id: number; title: string; type?: string | null }
41
+
42
+ export const IMPORT_TARGETS: Array<{ value: string; label: string }> = [
43
+ { value: "CONTACT_FIRST_NAME", label: "First name" }, { value: "CONTACT_LAST_NAME", label: "Last name" },
44
+ { value: "CONTACT_WORK_EMAIL", label: "Work email" }, { value: "CONTACT_PERSONAL_EMAILS", label: "Personal email" },
45
+ { value: "CONTACT_DIRECT_PHONE", label: "Direct phone" }, { value: "CONTACT_MOBILE_PHONE", label: "Mobile phone" },
46
+ { value: "CONTACT_JOB_TITLE", label: "Job title" }, { value: "CONTACT_JOB_DEPARTMENT", label: "Department" },
47
+ { value: "CONTACT_SENIORITY_LEVEL", label: "Seniority" }, { value: "CONTACT_LINKEDIN_URL", label: "LinkedIn URL" },
48
+ { value: "CONTACT_CITY", label: "City" }, { value: "CONTACT_STATE", label: "State" },
49
+ { value: "CONTACT_COUNTRY", label: "Country" }, { value: "COMPANY_DOMAIN", label: "Company domain" },
50
+ ];
51
+
52
+ const GUESSES: Array<[RegExp, string]> = [
53
+ [/^(work[\s_-]?)?e-?mail/i, "CONTACT_WORK_EMAIL"], [/first/i, "CONTACT_FIRST_NAME"], [/last|surname/i, "CONTACT_LAST_NAME"],
54
+ [/title|position/i, "CONTACT_JOB_TITLE"], [/linkedin/i, "CONTACT_LINKEDIN_URL"], [/mobile|cell/i, "CONTACT_MOBILE_PHONE"],
55
+ [/phone/i, "CONTACT_DIRECT_PHONE"], [/seniority|level/i, "CONTACT_SENIORITY_LEVEL"], [/department/i, "CONTACT_JOB_DEPARTMENT"],
56
+ [/city/i, "CONTACT_CITY"], [/state|region/i, "CONTACT_STATE"], [/country/i, "CONTACT_COUNTRY"], [/domain|website/i, "COMPANY_DOMAIN"],
57
+ ];
58
+
59
+ /** A first guess at where each file column goes; every target is used at most once. */
60
+ export function guessMapping(columns: string[]): Record<string, string> {
61
+ const used = new Set<string>();
62
+ const out: Record<string, string> = {};
63
+ for (const col of columns) {
64
+ const hit = GUESSES.find(([re, target]) => re.test(col) && !used.has(target));
65
+ if (hit) { out[col] = hit[1]; used.add(hit[1]); }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, className }: ImportWizardProps) {
71
+ const [tooBig, setTooBig] = useState<string | null>(null);
72
+ const [upload, setUpload] = useState<Upload | null>(null);
73
+ const [mapping, setMapping] = useState<Record<string, string>>({});
74
+ const [destination, setDestination] = useState<number | undefined>(listId);
75
+ const [newListTitle, setNewListTitle] = useState("");
76
+ const [policy, setPolicy] = useState<"autofill" | "override">("autofill");
77
+ const [enrich, setEnrich] = useState(false);
78
+ const [confirmOpen, setConfirmOpen] = useState(false);
79
+ const [result, setResult] = useState<ImportResult | null>(null);
80
+
81
+ const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }), [], { enabled: upload != null && listId == null });
82
+ const send = useG8Mutation((file: File) => { const body = new FormData(); body.append("file", file); return g8Post<Upload>("/imports", body); }, {
83
+ onSuccess: (r) => { setUpload(r.data); setMapping(guessMapping(r.data.columns ?? [])); },
84
+ });
85
+ const preview = useG8Mutation((m: Record<string, string>) => g8Post<Preview>(`/imports/${upload!.datafile_id}/preview`, m, { query: { page: 1, limit: 5 } }));
86
+ const createList = useG8Mutation((title: string) => g8Post<{ id: number }>("/lists", { title, type: "contacts" }));
87
+ const run = useG8Mutation(async () => {
88
+ let target = destination;
89
+ if (target == null && newListTitle.trim()) {
90
+ target = (await createList.mutateAsync(newListTitle.trim())).data.id;
91
+ setDestination(target); // a failed import must retry into THIS list, not create a second one
92
+ }
93
+ const res = await g8Post<ImportResult>(`/imports/${upload!.datafile_id}/contacts`, { list_id: target, column_mapping: mapping, update_policy: policy, enrich_from_graph8: enrich });
94
+ return res.data;
95
+ }, { onSuccess: (r) => { setConfirmOpen(false); setResult(r); onImported?.(r); } });
96
+
97
+ useEffect(() => { if (upload) void preview.mutate(mapping); }, [upload, mapping]); // eslint-disable-line react-hooks/exhaustive-deps
98
+
99
+ const mapped = Object.values(mapping).filter(Boolean);
100
+ const hasIdentity = mapped.includes("CONTACT_WORK_EMAIL") || mapped.includes("CONTACT_LINKEDIN_URL");
101
+ const ready = upload != null && hasIdentity && (destination != null || newListTitle.trim().length > 0);
102
+ const needsConfirm = policy === "override" || enrich;
103
+ const rows = upload?.row_count ?? preview.data?.data.total ?? 0;
104
+
105
+ if (result) {
106
+ return (
107
+ <div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
108
+ <h3 className="text-base font-semibold">Import finished</h3>
109
+ <p className="text-sm">{result.imported ?? 0} new, {result.updated ?? 0} updated, {result.skipped ?? 0} skipped, {result.failed ?? 0} failed.</p>
110
+ <Button variant="outline" onClick={() => { setResult(null); setUpload(null); setMapping({}); setDestination(listId); setNewListTitle(""); setPolicy("autofill"); setEnrich(false); run.reset(); preview.reset(); }}>Import another file</Button>
111
+ </div>
112
+ );
113
+ }
114
+ return (
115
+ <div className={`flex flex-col gap-5 ${className ?? ""}`}>
116
+ <section className="flex flex-col gap-2">
117
+ <Label htmlFor="import-file">1. File (CSV or Excel, up to {Math.round(maxBytes / 1024 / 1024)} MB)</Label>
118
+ <Input id="import-file" type="file" accept=".csv,.xlsx,.xls" onChange={(e) => {
119
+ const f = e.target.files?.[0];
120
+ if (!f) return;
121
+ if (f.size > maxBytes) return setTooBig(`${f.name} is ${(f.size / 1024 / 1024).toFixed(1)} MB. Files over ${Math.round(maxBytes / 1024 / 1024)} MB need a higher maxBodyBytes in the /api/g8 route (see the block README), or a server-side import.`);
122
+ setTooBig(null);
123
+ void send.mutate(f);
124
+ }} />
125
+ {tooBig ? <p role="alert" className="text-sm text-destructive">{tooBig}</p> : null}
126
+ {send.loading ? <p className="text-sm text-muted-foreground">Uploading...</p> : null}
127
+ {send.error ? <ApiError error={send.error} title="Could not read that file" /> : null}
128
+ </section>
129
+
130
+ {upload ? (
131
+ <>
132
+ <section className="flex flex-col gap-2">
133
+ <h3 className="text-sm font-medium">2. Map columns ({upload.row_count ?? "?"} rows)</h3>
134
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
135
+ {upload.columns.map((col) => (
136
+ <label key={col} className="flex items-center justify-between gap-2 text-sm">
137
+ <span className="truncate font-medium">{col}</span>
138
+ <select aria-label={`Map ${col}`} className="h-9 w-48 rounded-md border bg-background px-2" value={mapping[col] ?? ""}
139
+ onChange={(e) => setMapping((m) => { const next = { ...m }; if (e.target.value) next[col] = e.target.value; else delete next[col]; return next; })}>
140
+ <option value="">Skip this column</option>
141
+ {IMPORT_TARGETS.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
142
+ </select>
143
+ </label>
144
+ ))}
145
+ </div>
146
+ {!hasIdentity ? <p role="alert" className="text-sm text-destructive">Map a work email or a LinkedIn URL column so contacts can be matched.</p> : null}
147
+ {preview.error ? <ApiError error={preview.error} title="Could not preview the file" /> : null}
148
+ {preview.data?.data.rows.length ? (
149
+ <div className="overflow-x-auto rounded-md border">
150
+ <Table>
151
+ <TableHeader><TableRow>{Object.keys(preview.data.data.rows[0]).map((k) => <TableHead key={k}>{k}</TableHead>)}</TableRow></TableHeader>
152
+ <TableBody>{preview.data.data.rows.map((r, i) => <TableRow key={i}>{Object.values(r).map((v, j) => <TableCell key={j}>{String(v ?? "")}</TableCell>)}</TableRow>)}</TableBody>
153
+ </Table>
154
+ </div>
155
+ ) : null}
156
+ </section>
157
+
158
+ <section className="flex flex-col gap-3">
159
+ <h3 className="text-sm font-medium">3. Destination and rules</h3>
160
+ {listId == null ? (
161
+ <div className="flex flex-wrap items-center gap-2">
162
+ <select aria-label="Destination list" className="h-9 rounded-md border bg-background px-2 text-sm" value={destination ?? ""} onChange={(e) => setDestination(e.target.value ? Number(e.target.value) : undefined)}>
163
+ <option value="">New list...</option>
164
+ {(lists.data?.data ?? []).filter((l) => !l.type || l.type === "contacts").map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
165
+ </select>
166
+ {destination == null ? <Input aria-label="New list name" placeholder="New list name" className="w-56" value={newListTitle} onChange={(e) => setNewListTitle(e.target.value)} /> : null}
167
+ </div>
168
+ ) : null}
169
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={policy === "override"} onChange={(e) => setPolicy(e.target.checked ? "override" : "autofill")} /> Overwrite existing values (default only fills blanks)</label>
170
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={enrich} onChange={(e) => setEnrich(e.target.checked)} /> Fill missing fields from graph8 (uses credits)</label>
171
+ </section>
172
+
173
+ {run.error ? <ApiError error={run.error} title="The import failed" /> : null}
174
+ <div className="flex justify-end">
175
+ <Button disabled={!ready} loading={run.loading && !confirmOpen} onClick={() => (needsConfirm ? setConfirmOpen(true) : void run.mutate(undefined))}>Import {rows.toLocaleString()} rows</Button>
176
+ </div>
177
+ </>
178
+ ) : null}
179
+
180
+ <ConfirmSpendDialog
181
+ open={confirmOpen}
182
+ tier={enrich ? "billable" : "destructive"}
183
+ title={`Import ${rows.toLocaleString()} rows`}
184
+ description={policy === "override" ? "Matching contacts get their existing values replaced by the file." : "Missing fields are filled from graph8."}
185
+ affected={enrich ? undefined : rows}
186
+ confirming={run.loading}
187
+ confirmLabel="Import"
188
+ onConfirm={() => void run.mutate(undefined)}
189
+ onCancel={() => setConfirmOpen(false)}
190
+ />
191
+ </div>
192
+ );
193
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NotesPanel: list, add, edit and delete notes on a contact or a company.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listNotes | listCompanyNotes GET /contacts/{id}/notes | /companies/{id}/notes (read)
8
+ * 2. createNote | createCompanyNote POST same path (write)
9
+ * 3. updateNote PATCH /notes/{note_id} (write)
10
+ * 4. deleteNote DELETE /notes/{note_id} (destructive, ConfirmSpendDialog)
11
+ *
12
+ * `read_only` notes (projected from synced CRM activity) cannot be edited or deleted.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
19
+ import { EmptyState } from "@/g8/components/empty-state";
20
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
+
24
+ export interface NoteItem {
25
+ id: string;
26
+ content: string;
27
+ created_by_name?: string | null;
28
+ created_by?: string | null;
29
+ created_at?: string | null;
30
+ updated_at?: string | null;
31
+ read_only?: boolean;
32
+ }
33
+
34
+ export interface NotesPanelProps {
35
+ entity: "contact" | "company";
36
+ entityId: number;
37
+ /** Called after any write, so a sibling ActivityTimeline can reload. */
38
+ onChanged?: () => void;
39
+ className?: string;
40
+ }
41
+
42
+ export function NotesPanel({ entity, entityId, onChanged, className }: NotesPanelProps) {
43
+ const base = `/${entity === "contact" ? "contacts" : "companies"}/${entityId}/notes`;
44
+ const [draft, setDraft] = useState("");
45
+ const [editing, setEditing] = useState<{ id: string; content: string } | null>(null);
46
+ const [deleting, setDeleting] = useState<NoteItem | null>(null);
47
+
48
+ const notes = useG8Query((signal) => g8Get<NoteItem[]>(base, undefined, { signal }), [base]);
49
+ const done = () => { void notes.refetch(); onChanged?.(); };
50
+ const add = useG8Mutation((content: string) => g8Post(base, { content }), { onSuccess: () => { setDraft(""); done(); } });
51
+ const edit = useG8Mutation((v: { id: string; content: string }) => g8Patch(`/notes/${v.id}`, { content: v.content }), { onSuccess: () => { setEditing(null); done(); } });
52
+ const remove = useG8Mutation((id: string) => g8Delete(`/notes/${id}`), { onSuccess: () => { setDeleting(null); done(); } });
53
+
54
+ const items = (notes.data?.data ?? []) as NoteItem[];
55
+ return (
56
+ <div className={`flex flex-col gap-3 ${className ?? ""}`}>
57
+ <form
58
+ className="flex flex-col gap-2"
59
+ onSubmit={(e) => { e.preventDefault(); if (draft.trim()) void add.mutate(draft.trim()); }}
60
+ >
61
+ <Textarea aria-label="New note" placeholder="Write a note" rows={3} value={draft} onChange={(e) => setDraft(e.target.value)} />
62
+ <div className="flex justify-end"><Button type="submit" size="sm" loading={add.loading} disabled={!draft.trim()}>Add note</Button></div>
63
+ </form>
64
+ {add.error ? <ApiError error={add.error} title="Could not add the note" /> : null}
65
+ {edit.error ? <ApiError error={edit.error} title="Could not save the note" /> : null}
66
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the note" /> : null}
67
+ {notes.error ? <ApiError error={notes.error} title="Could not load notes" onRetry={() => void notes.refetch()} /> : null}
68
+ {notes.loading && !notes.data ? <Skeleton className="h-16 w-full" /> : null}
69
+ {!notes.loading && !notes.error && items.length === 0 ? <EmptyState title="No notes yet" description="Notes are visible to everyone who can see this record." /> : null}
70
+
71
+ <ul className="flex flex-col gap-2">
72
+ {items.map((n) => (
73
+ <li key={n.id} className="rounded-md border p-3" data-testid="note-item">
74
+ {editing?.id === n.id ? (
75
+ <div className="flex flex-col gap-2">
76
+ <Textarea aria-label="Edit note" rows={3} value={editing.content} onChange={(e) => setEditing({ id: n.id, content: e.target.value })} />
77
+ <div className="flex justify-end gap-2">
78
+ <Button variant="ghost" size="sm" onClick={() => setEditing(null)}>Cancel</Button>
79
+ <Button size="sm" loading={edit.loading} disabled={!editing.content.trim()} onClick={() => void edit.mutate(editing)}>Save</Button>
80
+ </div>
81
+ </div>
82
+ ) : (
83
+ <>
84
+ <p className="whitespace-pre-wrap text-sm">{n.content}</p>
85
+ <div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
86
+ <span>{n.created_by_name ?? n.created_by ?? "Unknown"}{n.created_at ? `, ${new Date(n.created_at).toLocaleString()}` : ""}</span>
87
+ {n.read_only ? <span>Synced, read only</span> : (
88
+ <span className="flex gap-1">
89
+ <Button variant="ghost" size="sm" onClick={() => setEditing({ id: n.id, content: n.content })}>Edit</Button>
90
+ <Button variant="ghost" size="sm" onClick={() => setDeleting(n)}>Delete</Button>
91
+ </span>
92
+ )}
93
+ </div>
94
+ </>
95
+ )}
96
+ </li>
97
+ ))}
98
+ </ul>
99
+
100
+ <ConfirmSpendDialog
101
+ open={deleting != null}
102
+ tier="destructive"
103
+ title="Delete this note"
104
+ description={deleting?.content.slice(0, 120)}
105
+ affected={1}
106
+ confirming={remove.loading}
107
+ onConfirm={() => deleting && void remove.mutate(deleting.id)}
108
+ onCancel={() => setDeleting(null)}
109
+ />
110
+ </div>
111
+ );
112
+ }