@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -18,6 +18,7 @@
18
18
  import { useEffect, useState } from "react";
19
19
  import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
20
20
  import { ApiError } from "@/g8/components/api-error";
21
+ import { ListSelect } from "@/g8/crm/components/list-select";
21
22
  import { g8Get, g8Post } from "@/g8/lib/g8-client";
22
23
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
24
  import { useG8Query } from "@/g8/hooks/use-g8-query";
@@ -29,7 +30,6 @@ export interface FormMappingEditorProps {
29
30
  onSaved?: () => void;
30
31
  }
31
32
 
32
- interface ListOption { id: number; title: string }
33
33
 
34
34
  const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
35
35
 
@@ -44,9 +44,9 @@ function FormMappingEditorFor({ form, onSaved }: FormMappingEditorProps) {
44
44
  const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, q, { signal }).then((r) => r.data), [base, form.source_id]);
45
45
  const fields = useG8Query((signal) => g8Get(`${base}/fields`, q, { signal }).then((r) => pickItems<{ name: string }>(r.data, "items", "fields")), [base, form.source_id]);
46
46
  const targets = useG8Query((signal) => g8Get<{ groups?: ContactFieldGroup[] }>("/contacts/form-fields", {}, { signal }).then((r) => r.data?.groups ?? []), []);
47
- const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
48
47
  const [rows, setRows] = useState<MappingRow[]>([]);
49
48
  const [listId, setListId] = useState<number | undefined>(undefined);
49
+ const [pickedTitle, setPickedTitle] = useState<string | undefined>(undefined);
50
50
  const [seeded, setSeeded] = useState(false);
51
51
 
52
52
  useEffect(() => {
@@ -60,7 +60,7 @@ function FormMappingEditorFor({ form, onSaved }: FormMappingEditorProps) {
60
60
  setSeeded(true);
61
61
  }, [overview.data, fields.data, seeded]);
62
62
 
63
- const listName = (lists.data ?? []).find((l) => l.id === listId)?.title ?? overview.data?.destination?.list_name ?? "";
63
+ const listName = pickedTitle ?? overview.data?.destination?.list_name ?? "";
64
64
  const body = mappingPayload(form.source_id, rows, listName, listId, overview.data?.route?.version ?? overview.data?.destination?.route_version);
65
65
  const ready = Boolean(listId) && body.form_to_contact_field_mapping.length > 0;
66
66
 
@@ -75,7 +75,7 @@ function FormMappingEditorFor({ form, onSaved }: FormMappingEditorProps) {
75
75
  const test = useG8Mutation(async () => (await g8Post(`${base}/route/test`, { source_id: form.source_id })).data);
76
76
 
77
77
  const set = (i: number, patch: Partial<MappingRow>): void => setRows((prev) => prev.map((r, at) => (at === i ? { ...r, ...patch } : r)));
78
- const loadError = overview.error ?? fields.error ?? targets.error ?? lists.error;
78
+ const loadError = overview.error ?? fields.error ?? targets.error;
79
79
  if (loadError) return <ApiError error={loadError} title="Could not load the form setup" onRetry={() => { void overview.refetch(); void fields.refetch(); }} />;
80
80
  if (!seeded) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-9 w-64" /><Skeleton className="h-32 w-full" /></div>;
81
81
 
@@ -86,10 +86,7 @@ function FormMappingEditorFor({ form, onSaved }: FormMappingEditorProps) {
86
86
  <CardContent className="grid gap-4">
87
87
  <div className="max-w-sm">
88
88
  <Label htmlFor="g8-form-list" className="mb-1">Save leads to list</Label>
89
- <select id="g8-form-list" className={SELECT} value={listId ?? ""} onChange={(e) => setListId(e.target.value ? Number(e.target.value) : undefined)}>
90
- <option value="">Choose a list...</option>
91
- {(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
92
- </select>
89
+ <ListSelect id="g8-form-list" label="Save leads to list" emptyOption="Choose a list..." pageSize={200} value={listId} valueLabel={overview.data?.destination?.list_name ?? undefined} selectClassName={SELECT} onChange={(l) => { setListId(l ? l.id : undefined); setPickedTitle(l ? l.title : undefined); }} />
93
90
  </div>
94
91
  {rows.length === 0 ? <p className="text-muted-foreground text-sm">No fields have been captured for this form yet. Submit it once on your site, then reload.</p> : (
95
92
  <div className="grid gap-2">
@@ -114,7 +111,7 @@ function FormMappingEditorFor({ form, onSaved }: FormMappingEditorProps) {
114
111
  <Button variant="ghost" size="sm" loading={test.loading} disabled={!overview.data?.destination?.list_id} onClick={() => void test.mutate(undefined)}>Send a test submission</Button>
115
112
  </div>
116
113
  {[suggest.error, preview.error, save.error, test.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
117
- {save.data ? <p role="status" className="text-sm text-green-700">Mapping saved. New submissions follow it.</p> : null}
114
+ {save.data ? <p role="status" className="text-sm text-positive-foreground">Mapping saved. New submissions follow it.</p> : null}
118
115
  {test.data ? <p role="status" className="text-sm">Test submission sent to the list (one synthetic contact was added). {cellText(test.data).slice(0, 200)}</p> : null}
119
116
  {previewRows.length > 0 ? (
120
117
  <div className="rounded border p-3 text-xs"><p className="mb-1 font-medium">Preview (nothing saved)</p>{previewRows.map((r, i) => <pre key={i} className="overflow-x-auto">{JSON.stringify(r)}</pre>)}</div>
@@ -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,12 +45,23 @@ 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";
63
+ import { pluralWord } from "@/g8/lib/plural";
64
+ import { useBrand } from "@/g8/hooks/use-brand";
50
65
 
51
66
  export interface FormsScreenProps {
52
67
  renderRowActions?: (form: FormListItem) => ReactNode;
@@ -56,7 +71,7 @@ export interface FormsScreenProps {
56
71
  className?: string;
57
72
  }
58
73
 
59
- type Tab = "forms" | "install" | "audit";
74
+ type Tab = "forms" | "install" | "audit" | "groups";
60
75
  const LIMIT = 100;
61
76
  /** How many of a form's latest submissions a deep link is looked up in. */
62
77
  const SUBMISSION_LOOKUP = 100;
@@ -204,8 +219,9 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
204
219
  }
205
220
 
206
221
  export function FormsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
222
+ const brand = useBrand();
207
223
  const [tabParam, setTabParam] = useUrlState("tab", "forms");
208
- const tab: Tab = tabParam === "install" || tabParam === "audit" ? tabParam : "forms";
224
+ const tab: Tab = tabParam === "install" || tabParam === "audit" || tabParam === "groups" ? tabParam : "forms";
209
225
  const setTab = (next: Tab): void => setTabParam(next === "forms" ? null : next);
210
226
  const [selectedKey, setSelectedKey] = useUrlState("id", "");
211
227
  const [view, setView] = useUrlState("view", "");
@@ -224,12 +240,18 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
224
240
  }, [text]);
225
241
 
226
242
  const closeForm = (): void => { setView(null); setSelectedKey(null); setSubmissionId(null); setOpenedSubmission(null); };
227
- const forms = useG8Query(
228
- (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 })),
229
- [search],
230
- );
231
- const items = forms.data?.items ?? [];
232
- const total = forms.data?.total ?? items.length;
243
+ // Every page of forms is reachable: the list loads LIMIT at a time and "Load more" appends the next page.
244
+ const [apiTotal, setTotal] = useState(0);
245
+ const forms = usePaginated<FormListItem>(async (cursor, signal) => {
246
+ const page = cursor ? Number(cursor) : 1;
247
+ const r = await g8Get<{ items?: FormListItem[]; total?: number }>("/forms", { page, per_page: LIMIT, search: search || undefined }, { signal });
248
+ const got = pickItems<FormListItem>(r.data, "items");
249
+ const all = (r.data as { total?: number } | undefined)?.total ?? 0;
250
+ setTotal(all);
251
+ return pageOf(got, got.length > 0 && page * LIMIT < all ? page + 1 : null);
252
+ }, [search]);
253
+ const items = forms.items;
254
+ const total = apiTotal || items.length;
233
255
  const shown = items.filter((f) => !health || formStatus(f) === health);
234
256
  const ref = parseRef(selectedKey);
235
257
  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;
@@ -247,13 +269,13 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
247
269
  );
248
270
  const linkedSubmission: FormSubmission | null = !submissionId ? null : openedSubmission?.id === submissionId ? openedSubmission : ((lookup.data ?? []).find((x) => x.id === submissionId) ?? null);
249
271
 
250
- const rows = forms.error ? <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} /> : forms.loading && !forms.data ? (
272
+ const rows = forms.error ? <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.reload()} /> : forms.loading && forms.items.length === 0 ? (
251
273
  <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>
252
274
  ) : shown.length === 0 ? (
253
275
  <EmptyState
254
276
  icon={<ClipboardList className="size-8" />}
255
277
  title={search || health ? "No forms match" : "No forms captured yet"}
256
- description={search || health ? "Try another name or health." : "Install the graph8 snippet on your site. Forms appear here after their first submission."}
278
+ description={search || health ? "Try another name or health." : `Install the ${brand.linkLabel} snippet on your site. Forms appear here after their first submission.`}
257
279
  action={!search && !health ? <Button onClick={() => setTab("install")}>Get the install code</Button> : undefined}
258
280
  />
259
281
  ) : (
@@ -267,7 +289,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
267
289
  <span className="block truncate font-medium">{formLabel(f)}</span>
268
290
  <span className="text-muted-foreground block truncate text-xs">{pageUrlText(f.page_url) || f.form_key}</span>
269
291
  <span className="text-muted-foreground block text-xs">
270
- {(f.total_submissions ?? 0).toLocaleString()} {(f.total_submissions ?? 0) === 1 ? "submission" : "submissions"}{f.failed_submissions ? ` · ${f.failed_submissions} failed` : ""}{f.target_list_name ? ` · to ${f.target_list_name}` : ""}
292
+ {(f.total_submissions ?? 0).toLocaleString()} {pluralWord(f.total_submissions ?? 0, "submission")}{f.failed_submissions ? ` · ${f.failed_submissions} failed` : ""}{f.target_list_name ? ` · to ${f.target_list_name}` : ""}
271
293
  </span>
272
294
  </button>
273
295
  <StatusPill tone={TONE[status]}>{STATUS_LABEL[status]}</StatusPill>
@@ -275,7 +297,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
275
297
  </li>
276
298
  );
277
299
  })}
278
- {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}
300
+ {forms.hasMore ? <li><MoreRows shown={items.length} noun={`of ${total.toLocaleString()} forms`} hasMore loading={forms.loadingMore} onMore={() => void forms.loadMore()} /></li> : null}
279
301
  </ul>
280
302
  );
281
303
 
@@ -288,9 +310,21 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
288
310
  onClose={() => closeForm()}
289
311
  {...expandProps}
290
312
  tabs={[
291
- { id: "overview", label: "Overview", content: <FormOverviewTab key={refKey(selected)} form={selected} onChanged={() => void forms.refetch()} /> },
292
- { id: "setup", label: "Destination and fields", content: <FormMappingEditor key={refKey(selected)} form={selected} onSaved={() => void forms.refetch()} /> },
293
- { id: "submissions", label: "Submissions", content: <SubmissionsTable key={`${refKey(selected)}:${subsRefresh}`} form={selected} selectedId={submissionId || undefined} onOpen={(sub) => { setOpenedSubmission(sub); setSubmissionId(sub.id); }} /> },
313
+ { id: "overview", label: "Overview", content: <FormOverviewTab key={refKey(selected)} form={selected} onChanged={() => void forms.reload()} /> },
314
+ { id: "setup", label: "Destination and fields", content: <FormMappingEditor key={refKey(selected)} form={selected} onSaved={() => void forms.reload()} /> },
315
+ { id: "submissions", label: "Submissions", content: (
316
+ <div className="grid gap-3">
317
+ <SubmissionsExport form={selected} />
318
+ <SubmissionsTable key={`${refKey(selected)}:${subsRefresh}`} form={selected} selectedId={submissionId || undefined} onOpen={(sub) => { setOpenedSubmission(sub); setSubmissionId(sub.id); }} />
319
+ </div>
320
+ ) },
321
+ { id: "routing", label: "Routing", content: (
322
+ <div className="grid gap-4">
323
+ <FormBackfill key={`b${refKey(selected)}`} form={selected} />
324
+ <FormRouteHistory key={`r${refKey(selected)}`} form={selected} onChanged={() => void forms.reload()} />
325
+ </div>
326
+ ) },
327
+ { id: "activity", label: "Activity", content: <FormActivity key={refKey(selected)} form={selected} /> },
294
328
  ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
295
329
  ]}
296
330
  />
@@ -302,7 +336,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
302
336
  title="Forms"
303
337
  description="Capture the forms already on your site, route every lead to a list, and watch what arrives."
304
338
  icon={<ClipboardList className="size-5" />}
305
- tabs={[{ id: "forms", label: "Forms", count: total }, { id: "install", label: "Install" }, { id: "audit", label: "Site audit" }]}
339
+ tabs={[{ id: "forms", label: "Forms", count: total }, { id: "groups", label: "Groups" }, { id: "install", label: "Install" }, { id: "audit", label: "Site audit" }]}
306
340
  activeTab={tab}
307
341
  onTabChange={(id) => { setTab(id as Tab); closeForm(); }}
308
342
  stats={tab === "forms" ? [
@@ -312,6 +346,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
312
346
  { label: "Need setup", value: items.filter((f) => formStatus(f) === "needs_setup").length.toLocaleString(), tone: "info" },
313
347
  ] : undefined}
314
348
  toolbar={tab === "forms" ? (
349
+ <>
315
350
  <Toolbar
316
351
  search={{ value: text, onChange: setText, placeholder: "Search forms" }}
317
352
  filters={(
@@ -322,6 +357,8 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
322
357
  )}
323
358
  actions={toolbarActions}
324
359
  />
360
+ <FormSavedViews className="mt-2" search={text} health={health} onApply={(v) => { setText(v.search); setSearch(v.search.trim()); setHealth(v.health); }} />
361
+ </>
325
362
  ) : undefined}
326
363
  >
327
364
  <FormSheet
@@ -342,7 +379,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
342
379
  )
343
380
  ) : null}
344
381
  </FormSheet>
345
- {tab === "install" ? <FormEmbed /> : tab === "audit" ? <SiteAuditPanel /> : (
382
+ {tab === "install" ? <FormEmbed /> : tab === "audit" ? <SiteAuditPanel /> : tab === "groups" ? <FormGroups /> : (
346
383
  <SplitView list={rows} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => closeForm()} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)} detailWidth="xl" />
347
384
  )}
348
385
  </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,9 +18,11 @@ 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";
25
+ import { useBrand } from "@/g8/hooks/use-brand";
24
26
 
25
27
  export interface SubmissionsTableProps {
26
28
  /** The form to show. Omit it (for example as a stack nav page) and a form picker is shown instead. */
@@ -63,18 +65,27 @@ function SubmissionTrace({ form, submission, onClose }: { form: FormRef; submiss
63
65
  );
64
66
  }
65
67
 
68
+ /** Forms per read: every page is read, so the form picker offers all of them. */
69
+ const FORM_PAGE = 100;
70
+
66
71
  export function SubmissionsTable({ form: given, pageSize = 20, maxFieldColumns = 4, onOpen, selectedId }: SubmissionsTableProps) {
72
+ const brand = useBrand();
67
73
  const [picked, setPicked] = useState<FormRef | undefined>(undefined);
68
- 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 });
69
80
  const form = given ?? picked;
70
81
  if (!form) {
71
82
  if (forms.error) return <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} />;
72
- if (forms.loading && !forms.data) return <Skeleton className="h-9 w-64" aria-busy="true" />;
73
- if ((forms.data ?? []).length === 0) return <EmptyState title="No forms captured yet" description="Install the graph8 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.`} />;
74
85
  return (
75
- <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 }); }}>
76
87
  <option value="" disabled>Choose a form</option>
77
- {(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>)}
78
89
  </select>
79
90
  );
80
91
  }
@@ -134,7 +145,7 @@ function SubmissionsFor({ form, pageSize, maxFieldColumns, onOpen, selectedId }:
134
145
  ) : <>{s.received_at ? new Date(s.received_at).toLocaleString() : ""}{s.is_test ? " (test)" : ""}</>}
135
146
  </TableCell>
136
147
  {columns.map((c) => <TableCell key={c}>{cellText(s.form_data?.[c])}</TableCell>)}
137
- <TableCell className={submissionFailed(s) ? "text-red-700" : undefined}>{submissionState(s)}</TableCell>
148
+ <TableCell className={submissionFailed(s) ? "text-error-foreground" : undefined}>{submissionState(s)}</TableCell>
138
149
  <TableCell className="text-right">
139
150
  <Button size="sm" variant="ghost" onClick={() => setTraceOf(s)}>Trace</Button>
140
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
+ }