@graph8/sdk 0.370.0 → 0.374.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SurveysScreen: the finished NPS and CSAT workspace. No props needed.
5
+ *
6
+ * Composes: ScreenLayout (title, tabs, stats), Toolbar (search, type, status and channel filters), SplitView + DetailPanel
7
+ * (the selected survey, deep link `?id=`), FormSheet (new survey via SurveyForm), SurveyResults, SurveyResponses, SurveySend,
8
+ * SurveyTriggers, SurveyForm in edit mode, OrgSummary, SendingDomains, AccountNps and ContactSurveyHistory.
9
+ *
10
+ * Tabs (`?tab=`): Surveys (list and detail), Overview (org-wide scores), Sending domains, Accounts and people.
11
+ * A survey's detail (`?id=`) has Results, Responses, Send, Automatic sends, Settings and Details; `&view=full` shows it as a page.
12
+ *
13
+ * User flows: see every survey with its type, state and channel; search and filter; create an NPS or CSAT survey; open one to read
14
+ * its score, response rate, split and trend; read, search, filter and export its answers (CSV); send it to a list now or later
15
+ * (confirmed with the audience size); add or remove automatic sends; edit its question, thank-you line and channel; close it
16
+ * (stops new sends, keeps every answer) and reopen it; delete it (confirmed); verify a sending domain; read org-wide NPS and CSAT;
17
+ * look up one company's NPS and one person's answers. The API has no share link, embed code, AI question writer or question
18
+ * list: a survey is one question (see the README for the gaps).
19
+ *
20
+ * SDK operations: listSurveys, updateSurvey (close and reopen), deleteSurvey (destructive, confirmed); everything else lives in
21
+ * the composed parts, which confirm every send, domain verification and removal.
22
+ */
23
+
24
+ import { useState } from "react";
25
+ import type { ReactNode } from "react";
26
+ import { ClipboardCheck, Lock, LockOpen, Plus, Trash2 } from "lucide-react";
27
+ import { Button, Skeleton } from "@graph8/sdk/ui";
28
+ import { ApiError } from "@/g8/components/api-error";
29
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
30
+ import { EmptyState } from "@/g8/components/empty-state";
31
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
32
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
33
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
34
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
35
+ import { SplitView } from "@/g8/components/screen/split-view";
36
+ import { StatusPill } from "@/g8/components/screen/status-pill";
37
+ import { Toolbar } from "@/g8/components/screen/toolbar";
38
+ import { useUrlState } from "@/g8/hooks/use-url-state";
39
+ import { g8Delete, g8Get, g8Patch } from "@/g8/lib/g8-client";
40
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
41
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
42
+ import { plural } from "@/g8/lib/plural";
43
+ import { COMMENT_MODES, channelLabel, isNps, statusOf, typeLabel, when } from "../lib/surveys-types";
44
+ import type { ResponseRow, Survey } from "../lib/surveys-types";
45
+ import { AccountNps } from "./account-nps";
46
+ import { ContactSurveyHistory } from "./contact-survey-history";
47
+ import { OrgSummary } from "./org-summary";
48
+ import { SendingDomains } from "./sending-domains";
49
+ import { SurveyForm } from "./survey-form";
50
+ import { SurveyResponses } from "./survey-responses";
51
+ import { SurveyResults } from "./survey-results";
52
+ import { SurveySend } from "./survey-send";
53
+ import { SurveyTriggers } from "./survey-triggers";
54
+
55
+ export interface SurveysScreenProps {
56
+ renderRowActions?: (survey: Survey) => ReactNode;
57
+ renderDetailActions?: (survey: Survey) => ReactNode;
58
+ /** Render a link to the person behind an answer (for example to your contact page). */
59
+ renderContact?: (row: ResponseRow) => ReactNode;
60
+ extraDetailTabs?: { id: string; label: string; render: (survey: Survey) => ReactNode }[];
61
+ toolbarActions?: ReactNode;
62
+ className?: string;
63
+ }
64
+
65
+ type Tab = "surveys" | "overview" | "domains" | "accounts";
66
+ const TABS: Tab[] = ["surveys", "overview", "domains", "accounts"];
67
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
68
+
69
+ export function SurveysScreen({ renderRowActions, renderDetailActions, renderContact, extraDetailTabs, toolbarActions, className }: SurveysScreenProps) {
70
+ const [id, setId] = useUrlState("id", "");
71
+ const [view, setView] = useUrlState("view", "");
72
+ const [tabParam, setTabParam] = useUrlState("tab", "surveys");
73
+ const tab = (TABS.includes(tabParam as Tab) ? tabParam : "surveys") as Tab;
74
+ const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
75
+ const [search, setSearch] = useState("");
76
+ const [typeFilter, setTypeFilter] = useState("");
77
+ const [statusFilter, setStatusFilter] = useState("");
78
+ const [channelFilter, setChannelFilter] = useState("");
79
+ const [creating, setCreating] = useState(false);
80
+ const [toDelete, setToDelete] = useState<Survey | null>(null);
81
+ const [busy, setBusy] = useState<string | null>(null);
82
+
83
+ const list = useG8Query((signal) => g8Get<Survey[]>("/feedback/surveys", {}, { signal }).then((r) => r.data ?? []), [], { key: ["surveys", "list"] });
84
+ const toggle = useG8Mutation((s: Survey) => g8Patch<Survey>(`/feedback/surveys/${encodeURIComponent(s.id)}`, { is_active: !s.is_active }), {
85
+ invalidates: (_r, s) => [["surveys", "list"], ["surveys", "detail", s.id]],
86
+ });
87
+ const remove = useG8Mutation((s: Survey) => g8Delete(`/feedback/surveys/${encodeURIComponent(s.id)}`), {
88
+ onSuccess: (_r, s) => { setToDelete(null); if (s.id === id) { setId(null); setView(null); } },
89
+ invalidates: [["surveys"]],
90
+ });
91
+ const flip = async (s: Survey): Promise<void> => {
92
+ setBusy(s.id);
93
+ await toggle.mutate(s);
94
+ setBusy(null);
95
+ };
96
+
97
+ const all = list.data ?? [];
98
+ const q = search.trim().toLowerCase();
99
+ const shown = all.filter((s) => (!typeFilter || s.type === typeFilter) && (!statusFilter || statusOf(s) === statusFilter) && (!channelFilter || s.delivery_channel === channelFilter) && (!q || `${s.name} ${s.question_text}`.toLowerCase().includes(q)));
100
+ const selected = all.find((s) => s.id === id);
101
+ const closeDetail = (): void => { setId(null); setView(null); };
102
+
103
+ const rows = list.error ? <ApiError error={list.error} title="Could not load surveys" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? (
104
+ <div aria-busy="true" className="grid gap-2"><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /></div>
105
+ ) : shown.length === 0 ? (
106
+ <EmptyState icon={<ClipboardCheck className="size-8" />} title={all.length === 0 ? "No surveys yet" : "No surveys match"} description={all.length === 0 ? "Ask customers how likely they are to recommend you (NPS) or how satisfied they are (CSAT), and read the answers here." : "Try another search or filter."} action={all.length === 0 ? <Button onClick={() => setCreating(true)}>Create your first survey</Button> : undefined} />
107
+ ) : (
108
+ <>
109
+ <p role="status" className="text-muted-foreground mb-2 text-xs">{shown.length === all.length ? `All ${plural(all.length, "survey")}` : `${shown.length.toLocaleString()} of ${plural(all.length, "survey")}`}</p>
110
+ <ul className="grid gap-2">
111
+ {shown.map((s) => (
112
+ <li key={s.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${s.id === id ? "bg-muted" : ""}`}>
113
+ <button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(s.id)}>
114
+ <span className="block truncate font-medium">{s.name}</span>
115
+ <span className="text-muted-foreground block truncate text-xs">{s.question_text} · {channelLabel(s.delivery_channel)}</span>
116
+ </button>
117
+ <StatusPill tone="info">{typeLabel(s)}</StatusPill>
118
+ <StatusPill tone={s.is_active ? "positive" : "neutral"}>{statusOf(s)}</StatusPill>
119
+ <div className="flex gap-1">
120
+ <Button size="sm" variant="ghost" aria-label={`${s.is_active ? "Close" : "Reopen"} ${s.name}`} loading={busy === s.id} disabled={busy !== null} onClick={() => void flip(s)}>{s.is_active ? <Lock className="size-4" /> : <LockOpen className="size-4" />}</Button>
121
+ <Button size="sm" variant="ghost" aria-label={`Delete ${s.name}`} onClick={() => setToDelete(s)}><Trash2 className="size-4" /></Button>
122
+ {renderRowActions?.(s)}
123
+ </div>
124
+ </li>
125
+ ))}
126
+ </ul>
127
+ </>
128
+ );
129
+
130
+ const detail = selected ? (
131
+ <DetailPanel
132
+ title={selected.name}
133
+ subtitle={selected.question_text}
134
+ badges={<><StatusPill tone="info">{typeLabel(selected)}</StatusPill><StatusPill tone={selected.is_active ? "positive" : "neutral"}>{statusOf(selected)}</StatusPill></>}
135
+ actions={<>{renderDetailActions?.(selected)}<Button size="sm" variant="outline" loading={busy === selected.id} disabled={busy !== null} onClick={() => void flip(selected)}>{selected.is_active ? <Lock className="size-4" /> : <LockOpen className="size-4" />}{selected.is_active ? "Close survey" : "Reopen survey"}</Button></>}
136
+ onClose={closeDetail}
137
+ {...expandProps}
138
+ tabs={[
139
+ { id: "results", label: "Results", content: <SurveyResults key={selected.id} survey={selected} /> },
140
+ { id: "responses", label: "Responses", content: <SurveyResponses key={selected.id} survey={selected} renderContact={renderContact} /> },
141
+ { id: "send", label: "Send", content: <SurveySend key={selected.id} survey={selected} /> },
142
+ { id: "automatic", label: "Automatic sends", content: <SurveyTriggers key={selected.id} survey={selected} /> },
143
+ { id: "settings", label: "Settings", content: <SurveyForm key={selected.id} survey={selected} onSaved={() => void list.refetch()} /> },
144
+ { id: "details", label: "Details", content: <SurveyDetails survey={selected} /> },
145
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
146
+ ]}
147
+ />
148
+ ) : null;
149
+
150
+ const surveysTab = (
151
+ <SplitView
152
+ list={(
153
+ <div className="grid gap-3">
154
+ <Toolbar
155
+ search={{ value: search, onChange: setSearch, placeholder: "Search surveys" }}
156
+ filters={(
157
+ <>
158
+ <select aria-label="Filter by type" className={SELECT} value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}><option value="">All types</option><option value="nps">NPS</option><option value="csat">CSAT</option></select>
159
+ <select aria-label="Filter by status" className={SELECT} value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}><option value="">Open and closed</option><option value="open">Open</option><option value="closed">Closed</option></select>
160
+ <select aria-label="Filter by channel" className={SELECT} value={channelFilter} onChange={(e) => setChannelFilter(e.target.value)}><option value="">All channels</option><option value="email">Email</option><option value="bridge">Chat message</option><option value="in_app">In-app</option></select>
161
+ </>
162
+ )}
163
+ actions={toolbarActions}
164
+ />
165
+ {rows}
166
+ </div>
167
+ )}
168
+ detail={detail}
169
+ detailOpen={Boolean(selected)}
170
+ onCloseDetail={closeDetail}
171
+ expanded={view === "full"}
172
+ onExpandedChange={(e) => setView(e ? "full" : null)}
173
+ detailWidth="xl"
174
+ />
175
+ );
176
+
177
+ return (
178
+ <ScreenLayout
179
+ className={className}
180
+ title="Surveys"
181
+ description="Ask customers how likely they are to recommend you (NPS) or how satisfied they are (CSAT)."
182
+ icon={<ClipboardCheck className="size-5" />}
183
+ actions={<Button size="sm" onClick={() => setCreating(true)}><Plus className="size-4" />New survey</Button>}
184
+ tabs={[{ id: "surveys", label: "Surveys", count: list.data ? all.length : undefined }, { id: "overview", label: "Overview" }, { id: "domains", label: "Sending domains" }, { id: "accounts", label: "Accounts and people" }]}
185
+ activeTab={tab}
186
+ onTabChange={(next) => { setTabParam(next === "surveys" ? null : next); closeDetail(); }}
187
+ stats={tab === "surveys" ? [
188
+ { label: "Surveys", value: list.data ? all.length.toLocaleString() : "..." },
189
+ { label: "Open", value: all.filter((s) => s.is_active).length.toLocaleString(), tone: "positive" },
190
+ { label: "Closed", value: all.filter((s) => !s.is_active).length.toLocaleString() },
191
+ { label: "NPS / CSAT", value: `${all.filter((s) => isNps(s)).length} / ${all.filter((s) => !isNps(s)).length}` },
192
+ ] : undefined}
193
+ >
194
+ {tab === "surveys" ? surveysTab : tab === "overview" ? <OrgSummary onOpenSurvey={(sid) => { setTabParam(null); setId(sid); }} /> : tab === "domains" ? <SendingDomains /> : (
195
+ <div className="grid gap-4 lg:grid-cols-2"><AccountNps /><ContactSurveyHistory /></div>
196
+ )}
197
+ <FormSheet open={creating} onOpenChange={setCreating} title="New survey" description="One question, scored. Nothing is sent until you send it.">
198
+ {creating ? <SurveyForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); setTabParam(null); void list.refetch(); setId(created.id); }} /> : null}
199
+ </FormSheet>
200
+ <ConfirmSpendDialog
201
+ open={toDelete !== null}
202
+ tier="destructive"
203
+ title={`Delete ${toDelete?.name ?? "this survey"}?`}
204
+ affected={1}
205
+ description={<><p>The survey, its automatic sends and every answer collected are removed. To stop asking but keep the answers, close it instead.</p>{remove.error ? <ApiError error={remove.error} title="Could not delete the survey" /> : null}</>}
206
+ confirmLabel="Delete"
207
+ confirming={remove.loading}
208
+ onCancel={() => setToDelete(null)}
209
+ onConfirm={() => { if (toDelete) void remove.mutate(toDelete); }}
210
+ />
211
+ {toggle.error ? <ApiError error={toggle.error} title="Could not change the survey" /> : null}
212
+ </ScreenLayout>
213
+ );
214
+ }
215
+
216
+ function SurveyDetails({ survey }: { survey: Survey }) {
217
+ return (
218
+ <KeyValueList
219
+ columns={2}
220
+ items={[
221
+ { label: "Name", value: survey.name },
222
+ { label: "Type", value: typeLabel(survey) },
223
+ { label: "Status", value: statusOf(survey) },
224
+ { label: "Question", value: survey.question_text, wide: true },
225
+ { label: "Scale", value: `${survey.scale_min} to ${survey.scale_max}` },
226
+ { label: "Shown through", value: channelLabel(survey.delivery_channel) },
227
+ { label: "Comments", value: COMMENT_MODES.find((m) => m.value === survey.comment_mode)?.label ?? "Score only" },
228
+ { label: "Thank-you line", value: survey.thank_you_copy || "", wide: true },
229
+ { label: "Created", value: when(survey.created_at) },
230
+ { label: "Survey id", value: survey.id, mono: true },
231
+ ]}
232
+ />
233
+ );
234
+ }