@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,72 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FormRouteHistory: undo a routing change. Restore the route the form had before its latest save, or stop routing the
5
+ * form altogether. The form keeps CAPTURING either way; only where leads are delivered changes.
6
+ *
7
+ * SDK operations, in the order the screen uses them:
8
+ * 1. getFormOverview GET /forms/{form_key}/overview?source_id (read; the current route version and destination)
9
+ * 2. rollbackFormRoute DELETE /forms/{form_key}/route { source_id, expected_current_version } (DESTRUCTIVE, confirmed: restores the previous route version)
10
+ * 3. rollbackFormRoute DELETE /forms/{form_key}/route { source_id, cancel_current: true, expected_current_version } (DESTRUCTIVE, confirmed: removes the route)
11
+ *
12
+ * Stopping routing sends the version the screen last read, so a change made in the meantime is refused (409). Restoring the
13
+ * previous route is NOT version-checked by the API: it always restores the version before the current one, so look at the
14
+ * route version shown here before you confirm. A refused request reloads the route and says what happened.
15
+ */
16
+ import { Button, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
19
+ import { useConfirmAction } from "@/g8/components/confirmed-action";
20
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { g8Delete, g8Get } from "@/g8/lib/g8-client";
24
+ import type { FormOverview, FormRef } from "../lib/form-types";
25
+
26
+ export interface FormRouteHistoryProps {
27
+ form: FormRef;
28
+ /** Called after the route changed, so the list and overview refresh. */
29
+ onChanged?: () => void;
30
+ className?: string;
31
+ }
32
+
33
+ export function FormRouteHistory({ form, onChanged, className }: FormRouteHistoryProps) {
34
+ const base = `/forms/${encodeURIComponent(form.form_key)}`;
35
+ const { ask, dialog, failure } = useConfirmAction();
36
+ const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, { source_id: form.source_id }, { signal }).then((r) => r.data), [form.form_key, form.source_id]);
37
+ const version = overview.data?.route?.version ?? undefined;
38
+ const list = overview.data?.destination?.list_name;
39
+ const done = (): void => { void overview.refetch(); onChanged?.(); };
40
+ const conflict = (err: unknown): unknown => {
41
+ if ((err as { status?: number })?.status !== 409) return err;
42
+ void overview.refetch();
43
+ return new Error("The API refused this: the route changed since it was loaded, or there is no earlier version to restore. The route shown here was reloaded: check it and try again.");
44
+ };
45
+
46
+ return (
47
+ <SectionCard className={className} title="Undo a routing change" description="Where this form's leads are delivered.">
48
+ {overview.error ? <ApiError error={overview.error} title="Could not load the route" onRetry={() => void overview.refetch()} /> : !overview.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : version === undefined ? (
49
+ <p className="text-sm">This form has no destination route to undo.</p>
50
+ ) : (
51
+ <div className="grid gap-3">
52
+ <KeyValueList columns={2} items={[{ label: "Route version", value: String(version) }, { label: "Destination list", value: list ?? "" }]} />
53
+ <div className="flex flex-wrap gap-2">
54
+ <Button variant="outline" disabled={version < 2} onClick={() => ask({
55
+ tier: "destructive", title: "Restore the previous route", confirmLabel: "Restore previous route",
56
+ description: `Replaces the current destination and field mapping with the ones this form had before its latest save. New leads follow the restored route; leads already delivered are not moved.`,
57
+ mapError: conflict, run: () => g8Delete(`${base}/route`, { body: { source_id: form.source_id } }), onDone: done,
58
+ })}>Restore the previous route</Button>
59
+ <Button variant="ghost" onClick={() => ask({
60
+ tier: "destructive", title: "Stop routing this form", confirmLabel: "Stop routing",
61
+ description: "Removes the destination and mapping. The form keeps capturing submissions, but they are not delivered to any list until you set a destination again.",
62
+ mapError: conflict, run: () => g8Delete(`${base}/route`, { body: { source_id: form.source_id, cancel_current: true, expected_current_version: version } }), onDone: done,
63
+ })}>Stop routing this form</Button>
64
+ </div>
65
+ {version < 2 ? <p className="text-muted-foreground text-xs">There is no earlier route to restore.</p> : null}
66
+ </div>
67
+ )}
68
+ {failure ? <ApiError error={failure} title="That did not work" onRetry={() => void overview.refetch()} /> : null}
69
+ <ConfirmSpendDialog {...dialog} />
70
+ </SectionCard>
71
+ );
72
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ /**
4
+ * FormSavedViews: save the current search and health filter of the forms list under a name, and bring it back with one
5
+ * click.
6
+ *
7
+ * SDK operations:
8
+ * 1. listSavedFormViews GET /forms/views/saved (read)
9
+ * 2. saveFormView PUT /forms/views/saved/{view_id} { name, filters } (write; a new view gets a fresh id, saving over a view keeps its id)
10
+ *
11
+ * The filters saved are the ones this list has: `search` and, for failing / needs setup / live, `saved_view`. Applying a
12
+ * view hands those back to the screen; nothing is written when a view is applied.
13
+ */
14
+ import { useState } from "react";
15
+ import { Button, Input } from "@graph8/sdk/ui";
16
+ import { ApiError } from "@/g8/components/api-error";
17
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { g8Get, g8Put } from "@/g8/lib/g8-client";
20
+ import { asRec, rowsOf, str } from "../lib/forms-extra";
21
+ import type { Rec } from "../lib/forms-extra";
22
+
23
+ export interface FormSavedViewsProps {
24
+ /** The list's current search text. */
25
+ search: string;
26
+ /** The list's current health filter ("" for all). */
27
+ health: string;
28
+ /** Apply a saved view back to the list. */
29
+ onApply: (view: { search: string; health: string }) => void;
30
+ className?: string;
31
+ }
32
+
33
+ /** Health values the API's `saved_view` filter understands. */
34
+ const HEALTH_VIEWS = new Set(["failing", "needs_setup", "live"]);
35
+
36
+ /** What a saved `saved_view` becomes in this list's health filter; values the list has no filter for map to "all". */
37
+ export function healthOfView(view: string): string {
38
+ if (view === "failing" || view === "failures") return "failing";
39
+ if (view === "needs_setup") return "needs_setup";
40
+ if (view === "live" || view === "healthy") return "live";
41
+ return "";
42
+ }
43
+
44
+ /** The list filters a view cannot hold (paused, no data): saving would silently drop them. */
45
+ export const UNSAVABLE_HEALTH = new Set(["paused", "no_data"]);
46
+
47
+ export function savedFilters(search: string, health: string): Rec {
48
+ return { ...(search.trim() ? { search: search.trim() } : {}), ...(HEALTH_VIEWS.has(health) ? { saved_view: health } : {}) };
49
+ }
50
+
51
+ function newViewId(): string {
52
+ return globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : `view-${Date.now().toString(36)}`;
53
+ }
54
+
55
+ export function FormSavedViews({ search, health, onApply, className }: FormSavedViewsProps) {
56
+ const views = useG8Query((signal) => g8Get("/forms/views/saved", undefined, { signal }), []);
57
+ const rows = rowsOf<Rec>(views.data?.data, "views");
58
+ const [name, setName] = useState("");
59
+ const filters = savedFilters(search, health);
60
+ const existing = rows.find((v) => str(v.name) === name.trim());
61
+ // Saving under a name that already exists keeps that view's id (an update); a new name gets a fresh id.
62
+ const save = useG8Mutation(() => g8Put(`/forms/views/saved/${encodeURIComponent(existing ? str(existing.id) : newViewId())}`, { name: name.trim(), filters }), {
63
+ onSuccess: () => { setName(""); void views.refetch(); },
64
+ });
65
+ return (
66
+ <div className={className}>
67
+ <div className="flex flex-wrap items-center gap-2">
68
+ {rows.map((v) => {
69
+ const f = asRec(v.filters);
70
+ return <Button key={str(v.id)} size="sm" variant="outline" onClick={() => onApply({ search: str(f.search), health: healthOfView(str(f.saved_view)) })}>{str(v.name)}</Button>;
71
+ })}
72
+ <Input aria-label="Name for this view" className="h-8 w-40" placeholder="Save this view as" value={name} onChange={(e) => setName(e.target.value)} />
73
+ <Button size="sm" variant="outline" loading={save.loading} disabled={name.trim() === "" || Object.keys(filters).length === 0 || UNSAVABLE_HEALTH.has(health)} onClick={() => void save.mutate(undefined)}>Save view</Button>
74
+ </div>
75
+ {UNSAVABLE_HEALTH.has(health) ? <p role="note" className="text-muted-foreground mt-1 text-xs">A view cannot hold the Paused or No data filter. Pick another health, or clear it, to save this view.</p> : null}
76
+ {Object.keys(filters).length === 0 && name.trim() !== "" ? <p className="text-muted-foreground mt-1 text-xs">Set a search or a health filter first: a view with no filters would show everything.</p> : null}
77
+ {views.error ? <p role="note" className="text-muted-foreground mt-1 text-xs">Saved views could not be loaded. <button type="button" className="underline" onClick={() => void views.refetch()}>Reload views</button></p> : null}
78
+ {save.error ? <ApiError error={save.error} title="Could not save the view" /> : null}
79
+ </div>
80
+ );
81
+ }
@@ -9,12 +9,16 @@
9
9
  * Composes: ScreenLayout (title, tabs, stats), Toolbar (debounced search, health filter), SplitView +
10
10
  * DetailPanel (selected form, deep link `?id=<source_id>/<form_key>`, one submission `&submission=<id>`), FormEmbed
11
11
  * (Install tab, `?tab=install`), SiteAuditPanel (Site audit tab, `?tab=audit`), FormMappingEditor (Setup tab),
12
- * SubmissionsTable (Submissions tab).
12
+ * SubmissionsTable + SubmissionsExport (Submissions tab), FormBackfill + FormRouteHistory (Routing tab), FormActivity
13
+ * (Activity tab), FormGroups (Groups tab, `?tab=groups`), FormSavedViews (saved search and health filters).
13
14
  *
14
15
  * User flows: install the snippet and copy the ready-made lead form, see every captured form with its health and
15
16
  * counts, search and filter, open one to read capture and delivery numbers, rename it, pause or resume delivery
16
17
  * (confirmed), pick the destination list and map fields (suggest, preview, save, test), read submissions, trace
17
- * where a lead went and retry a failed delivery (confirmed), and find forms graph8 is not capturing.
18
+ * where a lead went and retry a failed delivery (confirmed), export submissions to CSV, deliver earlier leads through
19
+ * the route (preview, then a confirmed delivery), undo a routing change (confirmed), read a form's activity and the pages
20
+ * it appears on, group related forms, save a filtered view, load every form past the first page, and find forms graph8
21
+ * is not capturing. Creating a form is not possible: forms are captured from your site (an API limit, not a missing screen).
18
22
  *
19
23
  * SDK operations: listForms, getFormOverview, updateFormRegistry, pauseFormDelivery / resumeFormDelivery
20
24
  * (external, confirmed), searchFormSubmissions (read, to resolve a `&submission=` deep link that was not clicked
@@ -41,11 +45,20 @@ import { useUrlState } from "@/g8/hooks/use-url-state";
41
45
  import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
42
46
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
43
47
  import { useG8Query } from "@/g8/hooks/use-g8-query";
48
+ import { usePaginated } from "@/g8/hooks/use-paginated";
49
+ import { pageOf } from "../lib/forms-extra";
44
50
  import { STATUS_LABEL, cellText, formLabel, formStatus, pageUrlText, pickItems, submissionFailed, submissionState } from "../lib/form-types";
45
51
  import type { FormListItem, FormOverview, FormRef, FormSubmission } from "../lib/form-types";
46
52
  import { FormEmbed } from "./form-embed";
53
+ import { FormActivity } from "./form-activity";
54
+ import { FormBackfill } from "./form-backfill";
55
+ import { FormGroups } from "./form-groups";
47
56
  import { FormMappingEditor } from "./form-mapping-editor";
57
+ import { FormRouteHistory } from "./form-route-history";
58
+ import { FormSavedViews } from "./form-saved-views";
59
+ import { MoreRows } from "./more-rows";
48
60
  import { SiteAuditPanel } from "./site-audit-panel";
61
+ import { SubmissionsExport } from "./submissions-export";
49
62
  import { SubmissionsTable } from "./submissions-table";
50
63
  import { pluralWord } from "@/g8/lib/plural";
51
64
  import { useBrand } from "@/g8/hooks/use-brand";
@@ -53,12 +66,14 @@ import { useBrand } from "@/g8/hooks/use-brand";
53
66
  export interface FormsScreenProps {
54
67
  renderRowActions?: (form: FormListItem) => ReactNode;
55
68
  renderDetailActions?: (form: FormListItem) => ReactNode;
69
+ /** Extra buttons on an opened submission, next to its delivery state (open or add the person who submitted it). */
70
+ renderSubmissionActions?: (submission: FormSubmission, form: FormListItem) => ReactNode;
56
71
  extraDetailTabs?: { id: string; label: string; render: (form: FormListItem) => ReactNode }[];
57
72
  toolbarActions?: ReactNode;
58
73
  className?: string;
59
74
  }
60
75
 
61
- type Tab = "forms" | "install" | "audit";
76
+ type Tab = "forms" | "install" | "audit" | "groups";
62
77
  const LIMIT = 100;
63
78
  /** How many of a form's latest submissions a deep link is looked up in. */
64
79
  const SUBMISSION_LOOKUP = 100;
@@ -151,7 +166,7 @@ function SubmissionNotFound({ form, submissionId }: { form: FormRef; submissionI
151
166
  }
152
167
 
153
168
  /** One submission in full: every submitted field, where it came from, its delivery, and the trace. */
154
- function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; submission: FormSubmission; onChanged: () => void }) {
169
+ function SubmissionDetail({ form, submission, onChanged, actions }: { form: FormRef; submission: FormSubmission; onChanged: () => void; actions?: ReactNode }) {
155
170
  const [retrying, setRetrying] = useState(false);
156
171
  const trace = useG8Query(
157
172
  (signal) => g8Get<{ capture_state?: string; receipts?: unknown[] }>(`/forms/${encodeURIComponent(form.form_key)}/submissions/${encodeURIComponent(submission.id)}/trace`, { source_id: form.source_id }, { signal }).then((r) => r.data),
@@ -167,6 +182,7 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
167
182
  <StatusPill tone={failed ? "negative" : submissionState(submission) === "delivered" ? "positive" : "neutral"}>{submissionState(submission)}</StatusPill>
168
183
  {submission.is_test ? <StatusPill tone="info">Test</StatusPill> : null}
169
184
  {failed && submission.delivery?.receipt_id ? <Button size="sm" variant="outline" onClick={() => { retry.reset(); setRetrying(true); }}>Retry delivery</Button> : null}
185
+ {actions}
170
186
  </div>
171
187
  <SectionCard title="Submitted fields">
172
188
  {fields.length === 0 ? <p className="text-muted-foreground text-sm">No fields were captured. Identifying fields may be redacted for your key.</p> : (
@@ -205,10 +221,10 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
205
221
  );
206
222
  }
207
223
 
208
- export function FormsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
224
+ export function FormsScreen({ renderRowActions, renderDetailActions, renderSubmissionActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
209
225
  const brand = useBrand();
210
226
  const [tabParam, setTabParam] = useUrlState("tab", "forms");
211
- const tab: Tab = tabParam === "install" || tabParam === "audit" ? tabParam : "forms";
227
+ const tab: Tab = tabParam === "install" || tabParam === "audit" || tabParam === "groups" ? tabParam : "forms";
212
228
  const setTab = (next: Tab): void => setTabParam(next === "forms" ? null : next);
213
229
  const [selectedKey, setSelectedKey] = useUrlState("id", "");
214
230
  const [view, setView] = useUrlState("view", "");
@@ -227,12 +243,18 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
227
243
  }, [text]);
228
244
 
229
245
  const closeForm = (): void => { setView(null); setSelectedKey(null); setSubmissionId(null); setOpenedSubmission(null); };
230
- const forms = useG8Query(
231
- (signal) => g8Get<{ items?: FormListItem[]; total?: number }>("/forms", { page: 1, per_page: LIMIT, search: search || undefined }, { signal }).then((r) => ({ items: pickItems<FormListItem>(r.data, "items"), total: (r.data as { total?: number } | undefined)?.total ?? 0 })),
232
- [search],
233
- );
234
- const items = forms.data?.items ?? [];
235
- const total = forms.data?.total ?? items.length;
246
+ // Every page of forms is reachable: the list loads LIMIT at a time and "Load more" appends the next page.
247
+ const [apiTotal, setTotal] = useState(0);
248
+ const forms = usePaginated<FormListItem>(async (cursor, signal) => {
249
+ const page = cursor ? Number(cursor) : 1;
250
+ const r = await g8Get<{ items?: FormListItem[]; total?: number }>("/forms", { page, per_page: LIMIT, search: search || undefined }, { signal });
251
+ const got = pickItems<FormListItem>(r.data, "items");
252
+ const all = (r.data as { total?: number } | undefined)?.total ?? 0;
253
+ setTotal(all);
254
+ return pageOf(got, got.length > 0 && page * LIMIT < all ? page + 1 : null);
255
+ }, [search]);
256
+ const items = forms.items;
257
+ const total = apiTotal || items.length;
236
258
  const shown = items.filter((f) => !health || formStatus(f) === health);
237
259
  const ref = parseRef(selectedKey);
238
260
  const selected: FormListItem | undefined = ref ? (items.find((f) => f.form_key === ref.form_key && f.source_id === ref.source_id) ?? { id: `${ref.source_id}:${ref.form_key}`, ...ref }) : undefined;
@@ -250,7 +272,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
250
272
  );
251
273
  const linkedSubmission: FormSubmission | null = !submissionId ? null : openedSubmission?.id === submissionId ? openedSubmission : ((lookup.data ?? []).find((x) => x.id === submissionId) ?? null);
252
274
 
253
- const rows = forms.error ? <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} /> : forms.loading && !forms.data ? (
275
+ const rows = forms.error ? <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.reload()} /> : forms.loading && forms.items.length === 0 ? (
254
276
  <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>
255
277
  ) : shown.length === 0 ? (
256
278
  <EmptyState
@@ -278,7 +300,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
278
300
  </li>
279
301
  );
280
302
  })}
281
- {total > items.length ? <li className="text-muted-foreground text-xs">Showing the first {items.length} of {total.toLocaleString()} forms. Search to narrow down.</li> : null}
303
+ {forms.hasMore ? <li><MoreRows shown={items.length} noun={`of ${total.toLocaleString()} forms`} hasMore loading={forms.loadingMore} onMore={() => void forms.loadMore()} /></li> : null}
282
304
  </ul>
283
305
  );
284
306
 
@@ -291,9 +313,21 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
291
313
  onClose={() => closeForm()}
292
314
  {...expandProps}
293
315
  tabs={[
294
- { id: "overview", label: "Overview", content: <FormOverviewTab key={refKey(selected)} form={selected} onChanged={() => void forms.refetch()} /> },
295
- { id: "setup", label: "Destination and fields", content: <FormMappingEditor key={refKey(selected)} form={selected} onSaved={() => void forms.refetch()} /> },
296
- { id: "submissions", label: "Submissions", content: <SubmissionsTable key={`${refKey(selected)}:${subsRefresh}`} form={selected} selectedId={submissionId || undefined} onOpen={(sub) => { setOpenedSubmission(sub); setSubmissionId(sub.id); }} /> },
316
+ { id: "overview", label: "Overview", content: <FormOverviewTab key={refKey(selected)} form={selected} onChanged={() => void forms.reload()} /> },
317
+ { id: "setup", label: "Destination and fields", content: <FormMappingEditor key={refKey(selected)} form={selected} onSaved={() => void forms.reload()} /> },
318
+ { id: "submissions", label: "Submissions", content: (
319
+ <div className="grid gap-3">
320
+ <SubmissionsExport form={selected} />
321
+ <SubmissionsTable key={`${refKey(selected)}:${subsRefresh}`} form={selected} selectedId={submissionId || undefined} onOpen={(sub) => { setOpenedSubmission(sub); setSubmissionId(sub.id); }} />
322
+ </div>
323
+ ) },
324
+ { id: "routing", label: "Routing", content: (
325
+ <div className="grid gap-4">
326
+ <FormBackfill key={`b${refKey(selected)}`} form={selected} />
327
+ <FormRouteHistory key={`r${refKey(selected)}`} form={selected} onChanged={() => void forms.reload()} />
328
+ </div>
329
+ ) },
330
+ { id: "activity", label: "Activity", content: <FormActivity key={refKey(selected)} form={selected} /> },
297
331
  ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
298
332
  ]}
299
333
  />
@@ -305,7 +339,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
305
339
  title="Forms"
306
340
  description="Capture the forms already on your site, route every lead to a list, and watch what arrives."
307
341
  icon={<ClipboardList className="size-5" />}
308
- tabs={[{ id: "forms", label: "Forms", count: total }, { id: "install", label: "Install" }, { id: "audit", label: "Site audit" }]}
342
+ tabs={[{ id: "forms", label: "Forms", count: total }, { id: "groups", label: "Groups" }, { id: "install", label: "Install" }, { id: "audit", label: "Site audit" }]}
309
343
  activeTab={tab}
310
344
  onTabChange={(id) => { setTab(id as Tab); closeForm(); }}
311
345
  stats={tab === "forms" ? [
@@ -315,6 +349,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
315
349
  { label: "Need setup", value: items.filter((f) => formStatus(f) === "needs_setup").length.toLocaleString(), tone: "info" },
316
350
  ] : undefined}
317
351
  toolbar={tab === "forms" ? (
352
+ <>
318
353
  <Toolbar
319
354
  search={{ value: text, onChange: setText, placeholder: "Search forms" }}
320
355
  filters={(
@@ -325,6 +360,8 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
325
360
  )}
326
361
  actions={toolbarActions}
327
362
  />
363
+ <FormSavedViews className="mt-2" search={text} health={health} onApply={(v) => { setText(v.search); setSearch(v.search.trim()); setHealth(v.health); }} />
364
+ </>
328
365
  ) : undefined}
329
366
  >
330
367
  <FormSheet
@@ -335,7 +372,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
335
372
  >
336
373
  {selected && submissionId ? (
337
374
  linkedSubmission ? (
338
- <SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} />
375
+ <SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} actions={renderSubmissionActions?.(linkedSubmission, selected)} />
339
376
  ) : lookup.error ? (
340
377
  <ApiError error={lookup.error} title="Could not load this submission" onRetry={() => void lookup.refetch()} />
341
378
  ) : lookup.loading || !lookup.data ? (
@@ -345,7 +382,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
345
382
  )
346
383
  ) : null}
347
384
  </FormSheet>
348
- {tab === "install" ? <FormEmbed /> : tab === "audit" ? <SiteAuditPanel /> : (
385
+ {tab === "install" ? <FormEmbed /> : tab === "audit" ? <SiteAuditPanel /> : tab === "groups" ? <FormGroups /> : (
349
386
  <SplitView list={rows} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => closeForm()} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)} detailWidth="xl" />
350
387
  )}
351
388
  </ScreenLayout>
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ /**
4
+ * MoreRows: the honest end of a list that the API pages. Shows how many rows are on screen and, while the API has
5
+ * another page, a "Load more" button. A list never ends silently: either every row is reachable or this says there are more.
6
+ * (Same contract as the foundation's LoadMore; kept local so the forms block does not wait on it.)
7
+ *
8
+ * SDK operations: none.
9
+ */
10
+ import { Button } from "@graph8/sdk/ui";
11
+
12
+ export interface MoreRowsProps {
13
+ /** Rows on screen. */
14
+ shown: number;
15
+ /** What the rows are, plural: "people". */
16
+ noun: string;
17
+ hasMore: boolean;
18
+ loading?: boolean;
19
+ onMore: () => void;
20
+ }
21
+
22
+ export function MoreRows({ shown, noun, hasMore, loading, onMore }: MoreRowsProps) {
23
+ if (!hasMore) return null;
24
+ return (
25
+ <div className="text-muted-foreground flex items-center justify-between gap-3 py-2 text-sm">
26
+ <span role="status">Showing {shown.toLocaleString()} {noun}</span>
27
+ <Button type="button" variant="outline" size="sm" loading={loading} onClick={onMore}>Load more</Button>
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SubmissionsExport: download a form's submissions as a CSV file (one column per submitted field).
5
+ *
6
+ * SDK operations:
7
+ * 1. searchFormSubmissions POST /forms/{form_key}/submissions { page, per_page: 100, source_id } (read; every page is fetched in turn)
8
+ *
9
+ * The export fetches up to `maxRows` submissions (default 5,000) and says so when the form has more: a truncated file
10
+ * is never presented as the whole thing. Identifying fields are redacted for keys without the Signals write capability,
11
+ * and the file carries the redacted values. Cells that start with = + - or @ are prefixed with ' so a spreadsheet does
12
+ * not run them as formulas.
13
+ */
14
+ import { useState } from "react";
15
+ import { Download } from "lucide-react";
16
+ import { Button } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
19
+ import { g8Post } from "@/g8/lib/g8-client";
20
+ import { submissionState } from "../lib/form-types";
21
+ import type { FormRef, FormSubmission } from "../lib/form-types";
22
+ import { submissionsCsv } from "../lib/forms-extra";
23
+
24
+ export interface SubmissionsExportProps {
25
+ form: FormRef;
26
+ /** The most submissions one file holds. Default 5000. */
27
+ maxRows?: number;
28
+ className?: string;
29
+ }
30
+
31
+ const PER_PAGE = 100;
32
+
33
+ export function SubmissionsExport({ form, maxRows = 5000, className }: SubmissionsExportProps) {
34
+ const [note, setNote] = useState("");
35
+ const run = useG8Mutation(async () => {
36
+ const rows: FormSubmission[] = [];
37
+ let total = 0;
38
+ for (let page = 1; rows.length < maxRows; page += 1) {
39
+ setNote(`Collecting submissions: ${rows.length.toLocaleString()}${total ? ` of ${total.toLocaleString()}` : ""}`);
40
+ const res = await g8Post<unknown>(`/forms/${encodeURIComponent(form.form_key)}/submissions`, { page, per_page: PER_PAGE, source_id: form.source_id });
41
+ const d = res.data as { submissions?: { items?: FormSubmission[]; total?: number }; items?: FormSubmission[]; total?: number } | undefined;
42
+ const block = d?.submissions ?? d;
43
+ const items = block?.items ?? [];
44
+ total = block?.total ?? total;
45
+ rows.push(...items);
46
+ if (items.length < PER_PAGE || (total && rows.length >= total)) break;
47
+ }
48
+ const file = rows.slice(0, maxRows);
49
+ const csv = submissionsCsv(file.map((s) => ({ id: s.id, received_at: s.received_at, is_test: s.is_test, delivery_state: submissionState(s), form_data: s.form_data })));
50
+ const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
51
+ const a = document.createElement("a");
52
+ a.href = url;
53
+ a.download = `${form.form_key.replace(/[^a-z0-9-_]+/gi, "-").toLowerCase() || "form"}-submissions.csv`;
54
+ a.click();
55
+ URL.revokeObjectURL(url);
56
+ setNote(total > file.length ? `Downloaded the first ${file.length.toLocaleString()} of ${total.toLocaleString()} submissions. The file does not hold the rest.` : `Downloaded ${file.length.toLocaleString()} submissions.`);
57
+ });
58
+ return (
59
+ <div className={className}>
60
+ <div className="flex items-center gap-3">
61
+ <Button size="sm" variant="outline" loading={run.loading} onClick={() => void run.mutate(undefined)}><Download className="size-4" />Export CSV</Button>
62
+ {note ? <span role="status" className="text-muted-foreground text-xs">{note}</span> : null}
63
+ </div>
64
+ {run.error ? <ApiError error={run.error} title="Could not export the submissions" /> : null}
65
+ </div>
66
+ );
67
+ }
@@ -18,6 +18,7 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
18
18
  import { EmptyState } from "@/g8/components/empty-state";
19
19
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
20
20
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
21
+ import { useAllPages } from "@/g8/hooks/use-all-pages";
21
22
  import { useG8Query } from "@/g8/hooks/use-g8-query";
22
23
  import { cellText, pickItems, submissionFailed, submissionState } from "../lib/form-types";
23
24
  import type { FormRef, FormSubmission } from "../lib/form-types";
@@ -64,19 +65,27 @@ function SubmissionTrace({ form, submission, onClose }: { form: FormRef; submiss
64
65
  );
65
66
  }
66
67
 
68
+ /** Forms per read: every page is read, so the form picker offers all of them. */
69
+ const FORM_PAGE = 100;
70
+
67
71
  export function SubmissionsTable({ form: given, pageSize = 20, maxFieldColumns = 4, onOpen, selectedId }: SubmissionsTableProps) {
68
72
  const brand = useBrand();
69
73
  const [picked, setPicked] = useState<FormRef | undefined>(undefined);
70
- const forms = useG8Query((signal) => g8Get<unknown>("/forms", { per_page: 100 }, { signal }).then((r) => pickItems<FormRef & { display_name?: string | null }>(r.data, "items")), [], { enabled: !given });
74
+ const forms = useAllPages<FormRef & { display_name?: string | null }>(async (page, signal) => {
75
+ const r = await g8Get<unknown>("/forms", { page, per_page: FORM_PAGE }, { signal });
76
+ const rows = pickItems<FormRef & { display_name?: string | null }>(r.data, "items");
77
+ const total = (r.data as { total?: number } | undefined)?.total;
78
+ return { rows, hasNext: typeof total === "number" ? page * FORM_PAGE < total : rows.length >= FORM_PAGE };
79
+ }, [], { enabled: !given });
71
80
  const form = given ?? picked;
72
81
  if (!form) {
73
82
  if (forms.error) return <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} />;
74
- if (forms.loading && !forms.data) return <Skeleton className="h-9 w-64" aria-busy="true" />;
75
- if ((forms.data ?? []).length === 0) return <EmptyState title="No forms captured yet" description={`Install the ${brand.linkLabel} snippet on your site; forms appear after their first submission.`} />;
83
+ if (forms.loading && !forms.loaded) return <Skeleton className="h-9 w-64" aria-busy="true" />;
84
+ if (forms.items.length === 0) return <EmptyState title="No forms captured yet" description={`Install the ${brand.linkLabel} snippet on your site; forms appear after their first submission.`} />;
76
85
  return (
77
- <select aria-label="Choose a form" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" defaultValue="" onChange={(e) => { const f = forms.data?.[Number(e.target.value)]; if (f) setPicked({ form_key: f.form_key, source_id: f.source_id }); }}>
86
+ <select aria-label="Choose a form" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" defaultValue="" onChange={(e) => { const f = forms.items[Number(e.target.value)]; if (f) setPicked({ form_key: f.form_key, source_id: f.source_id }); }}>
78
87
  <option value="" disabled>Choose a form</option>
79
- {(forms.data ?? []).map((f, i) => <option key={`${f.source_id}:${f.form_key}`} value={i}>{f.display_name || f.form_key}</option>)}
88
+ {forms.items.map((f, i) => <option key={`${f.source_id}:${f.form_key}`} value={i}>{f.display_name || f.form_key}</option>)}
80
89
  </select>
81
90
  );
82
91
  }
@@ -136,7 +145,7 @@ function SubmissionsFor({ form, pageSize, maxFieldColumns, onOpen, selectedId }:
136
145
  ) : <>{s.received_at ? new Date(s.received_at).toLocaleString() : ""}{s.is_test ? " (test)" : ""}</>}
137
146
  </TableCell>
138
147
  {columns.map((c) => <TableCell key={c}>{cellText(s.form_data?.[c])}</TableCell>)}
139
- <TableCell className={submissionFailed(s) ? "text-red-700" : undefined}>{submissionState(s)}</TableCell>
148
+ <TableCell className={submissionFailed(s) ? "text-error-foreground" : undefined}>{submissionState(s)}</TableCell>
140
149
  <TableCell className="text-right">
141
150
  <Button size="sm" variant="ghost" onClick={() => setTraceOf(s)}>Trace</Button>
142
151
  {submissionFailed(s) && s.delivery?.receipt_id ? <Button size="sm" variant="ghost" onClick={() => { retry.reset(); setRetrying(s); }}>Retry</Button> : null}
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Helpers for the forms management components (groups, activity, backfill, route history, saved views, export).
5
+ * These endpoints return open dicts, so every reader accepts what is present and ignores the rest.
6
+ */
7
+ import type { PaginatedResponse } from "@graph8/sdk";
8
+
9
+ export type Rec = Record<string, unknown>;
10
+
11
+ export function asRec(value: unknown): Rec {
12
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Rec) : {};
13
+ }
14
+
15
+ export function str(value: unknown, fallback = ""): string {
16
+ return typeof value === "string" ? value : value == null ? fallback : String(value);
17
+ }
18
+
19
+ export function num(value: unknown): number | undefined {
20
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
21
+ }
22
+
23
+ export function rowsOf<T = Rec>(data: unknown, ...keys: string[]): T[] {
24
+ if (Array.isArray(data)) return data as T[];
25
+ const rec = asRec(data);
26
+ for (const key of [...keys, "items"]) if (Array.isArray(rec[key])) return rec[key] as T[];
27
+ return [];
28
+ }
29
+
30
+ /** Wrap rows and the API's "next" marker (a page number, cursor or offset; null when there is no more) for `usePaginated`. */
31
+ export function pageOf<T>(rows: T[], next: unknown): PaginatedResponse<T> {
32
+ return { data: rows, pagination: { next_cursor: next == null || next === "" ? null : String(next) } } as unknown as PaginatedResponse<T>;
33
+ }
34
+
35
+ export const when = (v: unknown): string => {
36
+ const d = new Date(str(v));
37
+ return Number.isNaN(d.getTime()) ? "" : d.toLocaleString();
38
+ };
39
+
40
+ /** `YYYY-MM-DD` from a date input to the UTC instant the API takes: start of that day, or the start of the NEXT day for an inclusive end. */
41
+ export function dayInstant(day: string, endOfDay = false): string | undefined {
42
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return undefined;
43
+ const d = new Date(`${day}T00:00:00Z`);
44
+ if (Number.isNaN(d.getTime())) return undefined;
45
+ if (endOfDay) d.setUTCDate(d.getUTCDate() + 1);
46
+ return d.toISOString();
47
+ }
48
+
49
+ /** A member of a form group, under whichever key names it used. */
50
+ export function memberRef(m: Rec): { form_key: string; source_id: string } {
51
+ return { form_key: str(m.form_key), source_id: str(m.source_id) || str(m.source_key) };
52
+ }
53
+
54
+ /** One CSV cell: quoted when it holds a comma, quote or newline, and guarded against spreadsheet formulas. */
55
+ export function csvCell(value: unknown): string {
56
+ let s = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
57
+ if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
58
+ return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
59
+ }
60
+
61
+ /** The CSV text for submissions: a fixed leading set of columns, then every submitted field seen, in first-seen order. */
62
+ export function submissionsCsv(rows: Array<{ id: string; received_at?: string; is_test?: boolean; delivery_state: string; form_data?: Record<string, unknown> }>): string {
63
+ const fields = [...new Set(rows.flatMap((r) => Object.keys(r.form_data ?? {})))];
64
+ const head = ["submission_id", "received_at", "test", "delivery", ...fields];
65
+ const lines = rows.map((r) => [r.id, r.received_at ?? "", r.is_test ? "yes" : "no", r.delivery_state, ...fields.map((f) => r.form_data?.[f])].map(csvCell).join(","));
66
+ return [head.map(csvCell).join(","), ...lines].join("\r\n");
67
+ }