@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SurveyResults: the headline score, response rate, answer split and trend for one survey.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. getSurveySummary (GET /feedback/surveys/{survey_id}/summary?bucket) read score, response rate, split
8
+ * 2. getSurveyTrends (GET /feedback/surveys/{survey_id}/trends?bucket) read score by day, week or month
9
+ * A score the API leaves out (too few answers) shows a dash, never 0.
10
+ */
11
+
12
+ import { useState } from "react";
13
+ import { Skeleton } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { EmptyState } from "@/g8/components/empty-state";
16
+ import { BarList } from "@/g8/components/screen/bar-list";
17
+ import { SectionCard } from "@/g8/components/screen/section-card";
18
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { plural } from "@/g8/lib/plural";
22
+ import { BUCKETS, isNps, percent, scoreText } from "../lib/surveys-types";
23
+ import type { Bucket, Summary, Survey, Trends } from "../lib/surveys-types";
24
+
25
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
26
+
27
+ export function SurveyResults({ survey }: { survey: Pick<Survey, "id" | "type" | "name"> }) {
28
+ const [bucket, setBucket] = useState<Bucket>("week");
29
+ const summary = useG8Query((signal) => g8Get<Summary>(`/feedback/surveys/${encodeURIComponent(survey.id)}/summary`, { bucket }, { signal }).then((r) => r.data), [survey.id, bucket], { key: ["surveys", "summary", survey.id, bucket] });
30
+ const trends = useG8Query((signal) => g8Get<Trends>(`/feedback/surveys/${encodeURIComponent(survey.id)}/trends`, { bucket }, { signal }).then((r) => r.data), [survey.id, bucket], { key: ["surveys", "trends", survey.id, bucket] });
31
+ const nps = isNps(survey);
32
+
33
+ if (summary.error) return <ApiError error={summary.error} title="Could not load the results" onRetry={() => void summary.refetch()} />;
34
+ if (!summary.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
35
+ const s = summary.data;
36
+ const bars = trends.data?.bars ?? s.time_series ?? [];
37
+ const split = nps
38
+ ? [{ label: "Promoters (9 to 10)", value: s.promoters, tone: "positive" as const }, { label: "Passives (7 to 8)", value: s.passives }, { label: "Detractors (0 to 6)", value: s.detractors, tone: "negative" as const }]
39
+ : [{ label: "Satisfied (4 to 5)", value: s.satisfied ?? 0, tone: "positive" as const }, { label: "Neutral (3)", value: s.neutral ?? 0 }, { label: "Dissatisfied (1 to 2)", value: s.dissatisfied ?? 0, tone: "negative" as const }];
40
+ return (
41
+ <div className="grid gap-4">
42
+ <div className="flex items-center justify-end gap-2">
43
+ <label htmlFor="g8-svr-bucket" className="text-muted-foreground text-sm">Trend by</label>
44
+ <select id="g8-svr-bucket" aria-label="Trend bucket" className={SELECT} value={bucket} onChange={(e) => setBucket(e.target.value as Bucket)}>
45
+ {BUCKETS.map((b) => <option key={b.value} value={b.value}>{b.label}</option>)}
46
+ </select>
47
+ </div>
48
+ <StatStrip
49
+ items={[
50
+ nps ? { label: "NPS", value: scoreText(s.nps_score, 0), hint: "Promoters minus detractors, from -100 to 100. A dash means there are too few answers." } : { label: "Average CSAT", value: scoreText(s.csat_avg, 1), hint: "Mean score from 1 to 5." },
51
+ { label: "Answers", value: s.total_respondents.toLocaleString() },
52
+ { label: "Response rate", value: percent(s.response_rate.responded, s.response_rate.sent), hint: `${s.response_rate.responded.toLocaleString()} of ${s.response_rate.sent.toLocaleString()} people asked answered.` },
53
+ ]}
54
+ />
55
+ <SectionCard title="Who answered what" description={s.total_respondents ? plural(s.total_respondents, "answer") : undefined}>
56
+ {s.total_respondents === 0 ? <EmptyState title="No answers yet" description="Answers appear here as soon as people respond." /> : <BarList items={split} formatValue={(v) => v.toLocaleString()} emphasizeFirst={false} />}
57
+ </SectionCard>
58
+ <SectionCard title="Over time">
59
+ {trends.error ? <ApiError error={trends.error} title="Could not load the trend" onRetry={() => void trends.refetch()} /> : bars.length === 0 ? <p className="text-muted-foreground text-sm">No answers in this period yet.</p> : (
60
+ <table className="w-full text-sm">
61
+ <thead><tr className="text-muted-foreground text-left text-xs"><th className="py-1 font-normal">Period</th><th className="py-1 font-normal">Answers</th><th className="py-1 font-normal">{nps ? "NPS" : "Average"}</th></tr></thead>
62
+ <tbody>
63
+ {bars.map((b) => (
64
+ <tr key={b.period} className="border-t"><td className="py-1.5">{b.period}</td><td className="py-1.5">{b.total.toLocaleString()}</td><td className="py-1.5">{nps ? scoreText(b.nps_score, 0) : scoreText(b.csat_avg, 1)}</td></tr>
65
+ ))}
66
+ </tbody>
67
+ </table>
68
+ )}
69
+ </SectionCard>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SurveySendHistory: every send of one survey with its status, audience, how many people were reached and how many answered.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. getSurveySends (GET /feedback/surveys/{survey_id}/sends) read one unpaged read: the API returns the full history
8
+ */
9
+
10
+ import { RefreshCw } from "lucide-react";
11
+ import { Button, Skeleton } from "@graph8/sdk/ui";
12
+ import { ApiError } from "@/g8/components/api-error";
13
+ import { EmptyState } from "@/g8/components/empty-state";
14
+ import { SectionCard } from "@/g8/components/screen/section-card";
15
+ import { StatusPill } from "@/g8/components/screen/status-pill";
16
+ import type { Tone } from "@/g8/components/screen/status-pill";
17
+ import { g8Get } from "@/g8/lib/g8-client";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { plural } from "@/g8/lib/plural";
20
+ import { percent, when } from "../lib/surveys-types";
21
+ import type { SendRow } from "../lib/surveys-types";
22
+
23
+ const tone = (s: string): Tone => (s === "sent" ? "positive" : s === "failed" ? "negative" : s === "draft" ? "neutral" : "info");
24
+
25
+ export function SurveySendHistory({ surveyId }: { surveyId: string }) {
26
+ const sends = useG8Query(
27
+ (signal) => g8Get<{ sends?: SendRow[] }>(`/feedback/surveys/${encodeURIComponent(surveyId)}/sends`, {}, { signal }).then((r) => r.data?.sends ?? []),
28
+ [surveyId],
29
+ { key: ["surveys", "sends", surveyId] },
30
+ );
31
+ const rows = sends.data ?? [];
32
+ return (
33
+ <SectionCard
34
+ title="Send history"
35
+ description={sends.data ? `${plural(rows.length, "send")}, newest first. This is the complete history.` : undefined}
36
+ actions={<Button size="sm" variant="outline" loading={sends.loading && Boolean(sends.data)} onClick={() => void sends.refetch()}><RefreshCw className="size-4" />Refresh</Button>}
37
+ >
38
+ {sends.error ? <ApiError error={sends.error} title="Could not load the send history" onRetry={() => void sends.refetch()} /> : sends.loading && !sends.data ? (
39
+ <div aria-busy="true" className="grid gap-2"><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>
40
+ ) : rows.length === 0 ? <EmptyState title="Not sent yet" description="Sends appear here once you send this survey to a list." /> : (
41
+ <ul className="grid gap-2">
42
+ {[...rows].sort((a, b) => (b.created_at ?? "").localeCompare(a.created_at ?? "")).map((s) => (
43
+ <li key={s.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 text-sm">
44
+ <div className="min-w-0">
45
+ <p className="truncate font-medium">{s.audience_name || (s.trigger_kind ? `Automatic (${s.trigger_kind.replace(/_/g, " ")})` : "Audience list")}</p>
46
+ <p className="text-muted-foreground text-xs">
47
+ {s.scheduled_at ? `Scheduled ${when(s.scheduled_at)}` : when(s.created_at) || "Not dated"} · {plural(s.recipient_count ?? 0, "recipient")} · {s.responded_count ?? 0} answered ({percent(s.responded_count, s.recipient_count)})
48
+ {s.delivery_breakdown ? ` · ${s.delivery_breakdown.delivered ?? 0} delivered, ${s.delivery_breakdown.bounced ?? 0} bounced` : ""}
49
+ </p>
50
+ </div>
51
+ <StatusPill tone={tone(s.status)}>{s.status}</StatusPill>
52
+ </li>
53
+ ))}
54
+ </ul>
55
+ )}
56
+ </SectionCard>
57
+ );
58
+ }
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SurveySend: send a survey to an audience list now or later, then follow the send; the history of earlier sends sits below.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listSurveySendingDomains (GET /feedback/sending-domains) read the domains a sender address may use
8
+ * 2. listLists (GET /lists), through ListSelect read the audience
9
+ * 3. sendSurvey (POST /feedback/surveys/{survey_id}/send) external REAL MAIL to real people: ConfirmSpendDialog, sized from the chosen list
10
+ * 4. getSurveySend (GET /feedback/sends/{send_id}) read polled after the send is accepted
11
+ * The confirmation shows the audience size read from the list itself ("up to N": people who cannot be reached, such as
12
+ * unsubscribed or suppressed ones, are dropped by the API). An in-app survey is not sent: it shows inside your product.
13
+ */
14
+
15
+ import { useRef, useState } from "react";
16
+ import { Send } from "lucide-react";
17
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
18
+ import { ApiError } from "@/g8/components/api-error";
19
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
20
+ import { EmptyState } from "@/g8/components/empty-state";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { StatusPill } from "@/g8/components/screen/status-pill";
23
+ import { ListSelect } from "@/g8/crm/components/list-select";
24
+ import type { ListSelectOption } from "@/g8/crm/components/list-select";
25
+ import { g8Get, g8Post } from "@/g8/lib/g8-client";
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 { useBrand } from "@/g8/hooks/use-brand";
30
+ import { plural } from "@/g8/lib/plural";
31
+ import { EMAIL_RE, SEND_CHANNELS, audienceSize, domainOf, scheduleIso, when } from "../lib/surveys-types";
32
+ import type { SendAccepted, SendChannel, SendDetail, SendingDomain, Survey } from "../lib/surveys-types";
33
+ import { SurveySendHistory } from "./survey-send-history";
34
+
35
+ export interface SurveySendProps {
36
+ survey: Survey;
37
+ /** Called after a send is accepted, so a parent can refresh its numbers. */
38
+ onSent?: (accepted: SendAccepted) => void;
39
+ }
40
+
41
+ const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
42
+ const SETTLED = new Set(["sent", "failed", "scheduled"]);
43
+
44
+ export function SurveySend({ survey, onSent }: SurveySendProps) {
45
+ const brand = useBrand();
46
+ const [list, setList] = useState<ListSelectOption | null>(null);
47
+ const [channel, setChannel] = useState<SendChannel>(survey.delivery_channel === "bridge" ? "bridge" : "email");
48
+ const [senderEmail, setSenderEmail] = useState("");
49
+ const [senderName, setSenderName] = useState("");
50
+ const [replyTo, setReplyTo] = useState("");
51
+ const [subject, setSubject] = useState(survey.name);
52
+ const [at, setAt] = useState("");
53
+ const [confirm, setConfirm] = useState(false);
54
+ const [accepted, setAccepted] = useState<SendAccepted | null>(null);
55
+ const [historyKey, setHistoryKey] = useState(0);
56
+ // The poll reads the id from a ref: start() runs its first tick before React re-renders with the new state.
57
+ const sendId = useRef<number | null>(null);
58
+
59
+ const domains = useG8Query((signal) => g8Get<SendingDomain[]>("/feedback/sending-domains", {}, { signal }).then((r) => r.data ?? []), [], { key: ["surveys", "domains", "verified"] });
60
+ const status = useJobPoll<SendDetail>((signal) => g8Get<SendDetail>(`/feedback/sends/${sendId.current}`, {}, { signal }).then((r) => r.data), {
61
+ isTerminal: (s) => SETTLED.has(s.status),
62
+ enabled: false,
63
+ onDone: () => setHistoryKey((n) => n + 1),
64
+ });
65
+
66
+ const send = useG8Mutation(async (): Promise<SendAccepted> => {
67
+ const body: Record<string, unknown> = { audience_ref: String(list!.id), channel };
68
+ if (channel === "email") {
69
+ body.sender_email = senderEmail.trim();
70
+ body.subject = subject.trim();
71
+ if (senderName.trim()) body.sender_name = senderName.trim();
72
+ if (replyTo.trim()) body.reply_to = replyTo.trim();
73
+ }
74
+ const when_ = scheduleIso(at);
75
+ if (when_) body.scheduled_at = when_;
76
+ return (await g8Post<SendAccepted>(`/feedback/surveys/${encodeURIComponent(survey.id)}/send`, body)).data;
77
+ }, {
78
+ onSuccess: (r) => { sendId.current = r.send_id; setConfirm(false); setAccepted(r); setHistoryKey((n) => n + 1); onSent?.(r); status.start(); },
79
+ invalidates: [["surveys", "summary"]],
80
+ });
81
+
82
+ if (survey.delivery_channel === "in_app") {
83
+ return <EmptyState title="This survey is shown inside your product" description={`An in-app survey is not sent: the widget in ${brand.linkLabel} shows it to signed-in team members, and their answers appear under Responses. Change the survey to email or chat to send it to contacts.`} />;
84
+ }
85
+ const verified = (domains.data ?? []).filter((d) => d.status === "verified");
86
+ const domainOk = channel === "bridge" || (EMAIL_RE.test(senderEmail.trim()) && verified.some((d) => d.domain === domainOf(senderEmail.trim())));
87
+ const emailOk = channel === "bridge" || (domainOk && subject.trim() !== "" && (replyTo.trim() === "" || EMAIL_RE.test(replyTo.trim())));
88
+ const ready = Boolean(list) && emailOk && survey.is_active && (at === "" || (scheduleIso(at) !== undefined && new Date(at).getTime() > Date.now()));
89
+ const size = audienceSize(list);
90
+ const sendsLater = Boolean(scheduleIso(at));
91
+
92
+ return (
93
+ <div className="grid gap-4">
94
+ {!survey.is_active ? <p role="note" className="text-warning-foreground text-sm">This survey is closed. Reopen it before sending.</p> : null}
95
+ <SectionCard title="Send this survey" description="Choose who gets it. Nothing is sent until you confirm.">
96
+ <form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (ready) setConfirm(true); }}>
97
+ <div>
98
+ <Label htmlFor="g8-svs-list" className="mb-1">Audience list</Label>
99
+ <ListSelect id="g8-svs-list" label="Audience list" emptyOption="Choose a list" value={list?.id ?? ""} valueLabel={list?.title} selectClassName={SELECT} onChange={setList} />
100
+ {list && size === null ? <p className="text-muted-foreground mt-1 text-xs">The list does not report its size; the confirmation says so.</p> : null}
101
+ </div>
102
+ <div>
103
+ <Label htmlFor="g8-svs-channel" className="mb-1">Send as</Label>
104
+ <select id="g8-svs-channel" className={SELECT} value={channel} onChange={(e) => setChannel(e.target.value as SendChannel)}>
105
+ {SEND_CHANNELS.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}
106
+ </select>
107
+ </div>
108
+ {channel === "email" ? (
109
+ <>
110
+ <div className="grid gap-3 sm:grid-cols-2">
111
+ <div>
112
+ <Label htmlFor="g8-svs-from" className="mb-1">From address</Label>
113
+ <Input id="g8-svs-from" type="email" value={senderEmail} placeholder={verified[0] ? `survey@${verified[0].domain}` : "survey@yourdomain.com"} onChange={(e) => setSenderEmail(e.target.value)} />
114
+ </div>
115
+ <div><Label htmlFor="g8-svs-name" className="mb-1">From name (optional)</Label><Input id="g8-svs-name" value={senderName} onChange={(e) => setSenderName(e.target.value)} /></div>
116
+ </div>
117
+ {domains.loading && !domains.data ? <Skeleton className="h-4 w-48" /> : domains.error ? <ApiError error={domains.error} title="Could not load sending domains" onRetry={() => void domains.refetch()} /> : verified.length === 0 ? (
118
+ <p role="note" className="text-warning-foreground text-xs">No sending domain is verified yet. Verify one under Sending domains before sending email.</p>
119
+ ) : senderEmail.trim() && !domainOk ? (
120
+ <p role="alert" className="text-error text-xs">The address must be on a verified domain: {verified.map((d) => d.domain).join(", ")}.</p>
121
+ ) : <p className="text-muted-foreground text-xs">Verified domains: {verified.map((d) => d.domain).join(", ")}.</p>}
122
+ <div className="grid gap-3 sm:grid-cols-2">
123
+ <div><Label htmlFor="g8-svs-subject" className="mb-1">Subject</Label><Input id="g8-svs-subject" value={subject} onChange={(e) => setSubject(e.target.value)} /></div>
124
+ <div><Label htmlFor="g8-svs-reply" className="mb-1">Reply-to (optional)</Label><Input id="g8-svs-reply" type="email" value={replyTo} onChange={(e) => setReplyTo(e.target.value)} /></div>
125
+ </div>
126
+ </>
127
+ ) : null}
128
+ <div className="sm:w-64"><Label htmlFor="g8-svs-at" className="mb-1">Send later (optional)</Label><Input id="g8-svs-at" type="datetime-local" value={at} onChange={(e) => setAt(e.target.value)} /></div>
129
+ {at !== "" && scheduleIso(at) !== undefined && new Date(at).getTime() <= Date.now() ? <p role="alert" className="text-error text-xs">Pick a time in the future, or clear it to send now.</p> : null}
130
+ <div><Button type="submit" disabled={!ready}><Send className="size-4" />{sendsLater ? "Schedule send" : "Send survey"}</Button></div>
131
+ </form>
132
+ </SectionCard>
133
+ {accepted ? (
134
+ <SectionCard title="Last send">
135
+ <p className="flex flex-wrap items-center gap-2 text-sm" role="status">
136
+ <StatusPill tone={status.status?.status === "failed" ? "negative" : status.status?.status === "sent" ? "positive" : "info"}>{status.status?.status ?? accepted.status}</StatusPill>
137
+ {plural(status.status?.recipient_count ?? accepted.recipient_count, "recipient")}
138
+ {status.status?.scheduled_at ? `, scheduled for ${when(status.status.scheduled_at)}` : ""}
139
+ </p>
140
+ {status.error ? <ApiError error={status.error} title="Could not read the send status" onRetry={() => status.start()} /> : null}
141
+ </SectionCard>
142
+ ) : null}
143
+ <SurveySendHistory key={historyKey} surveyId={survey.id} />
144
+ <ConfirmSpendDialog
145
+ open={confirm}
146
+ tier="external"
147
+ title={`${sendsLater ? "Schedule" : "Send"} "${survey.name}" to ${list?.title ?? "this list"}?`}
148
+ recipients={size ?? undefined}
149
+ description={(
150
+ <>
151
+ <p>{size === null ? "The list does not report its size." : `Up to ${plural(size, "person", "people")} will be asked.`} People who unsubscribed or are suppressed are skipped. {sendsLater ? `It goes out on ${when(scheduleIso(at))}.` : "It goes out now and cannot be recalled."}</p>
152
+ {send.error ? <ApiError error={send.error} title="The send was refused" /> : null}
153
+ </>
154
+ )}
155
+ confirmLabel={sendsLater ? "Schedule" : "Send"}
156
+ confirming={send.loading}
157
+ onCancel={() => setConfirm(false)}
158
+ onConfirm={() => void send.mutate(undefined)}
159
+ />
160
+ </div>
161
+ );
162
+ }
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SurveyTriggers: automatic sends for one survey. A trigger mails an audience on a schedule, when someone joins a list, or
5
+ * when a record reaches a stage, without anyone pressing Send.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. listSurveyTriggers (GET /feedback/surveys/{survey_id}/triggers) read
9
+ * 2. listSurveySendingDomains (GET /feedback/sending-domains) read the sender must be on a verified domain
10
+ * 3. listLists (GET /lists), through ListSelect read
11
+ * 4. createSurveyTrigger (POST /feedback/surveys/{survey_id}/triggers) write confirmed here (external): it will mail people on its own
12
+ * 5. deleteSurveyTrigger (DELETE /feedback/surveys/{survey_id}/triggers/{id}) destructive ConfirmSpendDialog
13
+ * The API has no trigger update or pause: to change one, delete it and create it again.
14
+ */
15
+
16
+ import { useState } from "react";
17
+ import { Plus, Trash2, Zap } from "lucide-react";
18
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
23
+ import { SectionCard } from "@/g8/components/screen/section-card";
24
+ import { StatusPill } from "@/g8/components/screen/status-pill";
25
+ import { useAllLists } from "@/g8/crm/hooks/use-all-lists";
26
+ import { ListSelect } from "@/g8/crm/components/list-select";
27
+ import type { ListSelectOption } from "@/g8/crm/components/list-select";
28
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
29
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
30
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
31
+ import { plural } from "@/g8/lib/plural";
32
+ import { CADENCE_DAYS, EMAIL_RE, TRIGGER_KINDS, audienceSize, domainOf } from "../lib/surveys-types";
33
+ import type { SendingDomain, Survey, Trigger, TriggerKind } from "../lib/surveys-types";
34
+
35
+ const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
36
+ const kindLabel = (k: string): string => TRIGGER_KINDS.find((t) => t.value === k)?.label ?? k;
37
+
38
+ export function SurveyTriggers({ survey }: { survey: Survey }) {
39
+ const [creating, setCreating] = useState(false);
40
+ const [confirmNew, setConfirmNew] = useState(false);
41
+ const [toDelete, setToDelete] = useState<Trigger | null>(null);
42
+ const [kind, setKind] = useState<TriggerKind>("scheduled");
43
+ const [list, setList] = useState<ListSelectOption | null>(null);
44
+ const [cadence, setCadence] = useState<number>(90);
45
+ const [stage, setStage] = useState("");
46
+ const [senderEmail, setSenderEmail] = useState("");
47
+ const [senderName, setSenderName] = useState("");
48
+ const [replyTo, setReplyTo] = useState("");
49
+ const [subject, setSubject] = useState(survey.name);
50
+
51
+ const triggers = useG8Query((signal) => g8Get<Trigger[]>(`/feedback/surveys/${encodeURIComponent(survey.id)}/triggers`, {}, { signal }).then((r) => r.data ?? []), [survey.id], { key: ["surveys", "triggers", survey.id] });
52
+ const domains = useG8Query((signal) => g8Get<SendingDomain[]>("/feedback/sending-domains", {}, { signal }).then((r) => r.data ?? []), [], { key: ["surveys", "domains", "verified"], enabled: creating });
53
+
54
+ const lists = useAllLists([]);
55
+ const needsList = kind !== "stage_entry";
56
+ const verified = (domains.data ?? []).filter((d) => d.status === "verified");
57
+ const senderOk = EMAIL_RE.test(senderEmail.trim()) && verified.some((d) => d.domain === domainOf(senderEmail.trim()));
58
+ const valid = senderOk && subject.trim() !== "" && (!needsList || Boolean(list)) && (kind !== "stage_entry" || stage.trim() !== "") && (replyTo.trim() === "" || EMAIL_RE.test(replyTo.trim()));
59
+ const reset = (): void => { setCreating(false); setConfirmNew(false); setList(null); setStage(""); setSenderEmail(""); setSenderName(""); setReplyTo(""); setSubject(survey.name); };
60
+
61
+ const create = useG8Mutation(async (): Promise<Trigger> => {
62
+ // Only the fields this kind uses: a stage value on a scheduled trigger would be stored and misread later.
63
+ const body: Record<string, unknown> = { trigger_kind: kind, sender_email: senderEmail.trim(), subject: subject.trim() };
64
+ if (senderName.trim()) body.sender_name = senderName.trim();
65
+ if (replyTo.trim()) body.reply_to = replyTo.trim();
66
+ if (needsList) body.audience_ref = String(list!.id);
67
+ if (kind === "scheduled") body.cadence_days = cadence;
68
+ if (kind === "stage_entry") body.stage_value = stage.trim();
69
+ return (await g8Post<Trigger>(`/feedback/surveys/${encodeURIComponent(survey.id)}/triggers`, body)).data;
70
+ }, { onSuccess: reset, invalidates: [["surveys", "triggers", survey.id]] });
71
+ const remove = useG8Mutation((t: Trigger) => g8Delete(`/feedback/surveys/${encodeURIComponent(survey.id)}/triggers/${encodeURIComponent(t.id)}`), {
72
+ onSuccess: () => setToDelete(null),
73
+ invalidates: [["surveys", "triggers", survey.id]],
74
+ });
75
+
76
+ const rows = triggers.data ?? [];
77
+ const size = audienceSize(list);
78
+ return (
79
+ <SectionCard
80
+ title="Automatic sends"
81
+ description="Mail this survey without pressing Send."
82
+ actions={<Button size="sm" onClick={() => setCreating(true)} disabled={survey.delivery_channel !== "email"}><Plus className="size-4" />New automatic send</Button>}
83
+ >
84
+ {survey.delivery_channel !== "email" ? <p className="text-muted-foreground mb-2 text-xs">Automatic sends go out by email. Switch this survey to email to add one.</p> : null}
85
+ {triggers.error ? <ApiError error={triggers.error} title="Could not load automatic sends" onRetry={() => void triggers.refetch()} /> : triggers.loading && !triggers.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : rows.length === 0 ? (
86
+ <EmptyState icon={<Zap className="size-8" />} title="No automatic sends" description="Add one to ask a list every 30, 60 or 90 days, or whenever someone joins a list or reaches a stage." />
87
+ ) : (
88
+ <ul className="grid gap-2">
89
+ {rows.map((t) => (
90
+ <li key={t.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 text-sm">
91
+ <div className="min-w-0">
92
+ <p className="font-medium">{kindLabel(t.trigger_kind)}{t.cadence_days ? `, every ${plural(t.cadence_days, "day")}` : ""}{t.stage_value ? `: ${t.stage_value}` : ""}</p>
93
+ <p className="text-muted-foreground truncate text-xs">From {t.sender_email} · {t.subject}{t.audience_ref ? ` · ${lists.items.find((l) => String(l.id) === t.audience_ref)?.title ?? "an audience list"}` : ""}</p>
94
+ </div>
95
+ <div className="flex items-center gap-2">
96
+ <StatusPill tone={t.is_active ? "positive" : "neutral"}>{t.is_active ? "active" : "inactive"}</StatusPill>
97
+ <Button size="sm" variant="ghost" aria-label={`Remove automatic send ${kindLabel(t.trigger_kind)}`} onClick={() => setToDelete(t)}><Trash2 className="size-4" /></Button>
98
+ </div>
99
+ </li>
100
+ ))}
101
+ </ul>
102
+ )}
103
+ <FormSheet
104
+ open={creating}
105
+ onOpenChange={(o) => { if (!o) reset(); else setCreating(true); }}
106
+ title="New automatic send"
107
+ description="Once saved, this mails people on its own. You confirm it first."
108
+ footer={<Button disabled={!valid} onClick={() => setConfirmNew(true)}>Add automatic send</Button>}
109
+ >
110
+ <div className="grid gap-3">
111
+ <div>
112
+ <Label htmlFor="g8-svt-kind" className="mb-1">When</Label>
113
+ <select id="g8-svt-kind" className={SELECT} value={kind} onChange={(e) => setKind(e.target.value as TriggerKind)}>
114
+ {TRIGGER_KINDS.map((k) => <option key={k.value} value={k.value}>{k.label}</option>)}
115
+ </select>
116
+ <p className="text-muted-foreground mt-1 text-xs">{TRIGGER_KINDS.find((k) => k.value === kind)?.hint}</p>
117
+ </div>
118
+ {needsList ? (
119
+ <div>
120
+ <Label htmlFor="g8-svt-list" className="mb-1">{kind === "list_entry" ? "Watched list" : "Audience list"}</Label>
121
+ <ListSelect id="g8-svt-list" label={kind === "list_entry" ? "Watched list" : "Audience list"} emptyOption="Choose a list" value={list?.id ?? ""} valueLabel={list?.title} selectClassName={SELECT} onChange={setList} />
122
+ </div>
123
+ ) : null}
124
+ {kind === "scheduled" ? (
125
+ <div>
126
+ <Label htmlFor="g8-svt-cadence" className="mb-1">Repeat every</Label>
127
+ <select id="g8-svt-cadence" className={SELECT} value={cadence} onChange={(e) => setCadence(Number(e.target.value))}>
128
+ {CADENCE_DAYS.map((d) => <option key={d} value={d}>{d} days</option>)}
129
+ </select>
130
+ </div>
131
+ ) : null}
132
+ {kind === "stage_entry" ? <div><Label htmlFor="g8-svt-stage" className="mb-1">Stage</Label><Input id="g8-svt-stage" value={stage} placeholder="closed-won" onChange={(e) => setStage(e.target.value)} /></div> : null}
133
+ <div><Label htmlFor="g8-svt-from" className="mb-1">From address</Label><Input id="g8-svt-from" type="email" value={senderEmail} onChange={(e) => setSenderEmail(e.target.value)} /></div>
134
+ {domains.data && senderEmail.trim() && !senderOk ? <p role="alert" className="text-error text-xs">{verified.length ? `Use an address on a verified domain: ${verified.map((d) => d.domain).join(", ")}.` : "No sending domain is verified yet. Verify one under Sending domains."}</p> : null}
135
+ <div className="grid gap-3 sm:grid-cols-2">
136
+ <div><Label htmlFor="g8-svt-name" className="mb-1">From name (optional)</Label><Input id="g8-svt-name" value={senderName} onChange={(e) => setSenderName(e.target.value)} /></div>
137
+ <div><Label htmlFor="g8-svt-reply" className="mb-1">Reply-to (optional)</Label><Input id="g8-svt-reply" type="email" value={replyTo} onChange={(e) => setReplyTo(e.target.value)} /></div>
138
+ </div>
139
+ <div><Label htmlFor="g8-svt-subject" className="mb-1">Subject</Label><Input id="g8-svt-subject" value={subject} onChange={(e) => setSubject(e.target.value)} /></div>
140
+ </div>
141
+ </FormSheet>
142
+ <ConfirmSpendDialog
143
+ open={confirmNew}
144
+ tier="external"
145
+ title={`Add an automatic send for "${survey.name}"?`}
146
+ recipients={needsList ? (size ?? undefined) : undefined}
147
+ description={<><p>{kindLabel(kind)}: this survey is emailed from {senderEmail.trim() || "the sender"} without anyone pressing Send.{needsList && size !== null ? ` The list has ${plural(size, "person", "people")} now.` : ""}</p>{create.error ? <ApiError error={create.error} title="The automatic send was refused" /> : null}</>}
148
+ confirmLabel="Add"
149
+ confirming={create.loading}
150
+ onCancel={() => setConfirmNew(false)}
151
+ onConfirm={() => void create.mutate(undefined)}
152
+ />
153
+ <ConfirmSpendDialog
154
+ open={toDelete !== null}
155
+ tier="destructive"
156
+ title={`Remove this automatic send?`}
157
+ affected={1}
158
+ description={<><p>{toDelete ? `${kindLabel(toDelete.trigger_kind)} stops. Surveys already sent and their answers stay.` : ""}</p>{remove.error ? <ApiError error={remove.error} title="Could not remove it" /> : null}</>}
159
+ confirmLabel="Remove"
160
+ confirming={remove.loading}
161
+ onCancel={() => setToDelete(null)}
162
+ onConfirm={() => { if (toDelete) void remove.mutate(toDelete); }}
163
+ />
164
+ </SectionCard>
165
+ );
166
+ }