@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SubscriberTimeline: what one subscriber did with one sent issue (delivered, opened, clicked...), in order.
5
+ * Pick the issue from the ones already sent.
6
+ *
7
+ * SDK operations:
8
+ * 1. getSubscriberTimeline GET /newsletters/{id}/issues/{issue}/analytics/subscribers/{subscriber}/timeline (read)
9
+ *
10
+ * The payload is open-shaped: events are read from `events` (or `timeline`, or a bare list) and shown with whatever
11
+ * of `type`, `event`, `occurred_at`, `url` is present.
12
+ */
13
+ import { useEffect, useState } from "react";
14
+ import { Skeleton } from "@graph8/sdk/ui";
15
+ import { ApiError } from "@/g8/components/api-error";
16
+ import { EmptyState } from "@/g8/components/empty-state";
17
+ import { SectionCard } from "@/g8/components/screen/section-card";
18
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
19
+ import { g8Get } from "@/g8/lib/g8-client";
20
+ import { issueTitle } from "../lib/newsletter-types";
21
+ import type { Issue } from "../lib/newsletter-types";
22
+ import { rowsOf, str } from "../lib/newsletter-extra";
23
+ import type { Rec } from "../lib/newsletter-extra";
24
+
25
+ export interface SubscriberTimelineProps {
26
+ newsletterId: string;
27
+ subscriberId: number;
28
+ /** The issues to choose from (only sent ones are offered). */
29
+ issues: Issue[];
30
+ className?: string;
31
+ }
32
+
33
+ const when = (v: unknown): string => {
34
+ const d = new Date(str(v));
35
+ return Number.isNaN(d.getTime()) ? "" : d.toLocaleString();
36
+ };
37
+
38
+ export function SubscriberTimeline({ newsletterId, subscriberId, issues, className }: SubscriberTimelineProps) {
39
+ const sent = issues.filter((i) => i.status === "sent" || i.sent_at);
40
+ const [issueId, setIssueId] = useState<string>(sent[0] ? String(sent[0].id) : "");
41
+ // On a deep link the issues arrive after the first render: pick the newest sent one once they do.
42
+ useEffect(() => { if (issueId === "" && sent[0]) setIssueId(String(sent[0].id)); }, [issueId, sent.length]); // eslint-disable-line react-hooks/exhaustive-deps
43
+ const res = useG8Query(
44
+ (signal) => g8Get(`/newsletters/${encodeURIComponent(newsletterId)}/issues/${issueId}/analytics/subscribers/${subscriberId}/timeline`, undefined, { signal }),
45
+ [newsletterId, issueId, subscriberId],
46
+ { enabled: issueId !== "" },
47
+ );
48
+ const events = rowsOf<Rec>(res.data?.data, "events", "timeline");
49
+ return (
50
+ <SectionCard className={className} title="Activity on an issue" description="What this person did with one sent issue."
51
+ actions={sent.length ? <select aria-label="Issue" className="border-input bg-background h-8 rounded-md border px-2 text-sm" value={issueId} onChange={(e) => setIssueId(e.target.value)}>{sent.map((i) => <option key={i.id} value={String(i.id)}>{issueTitle(i)}</option>)}</select> : undefined}>
52
+ {sent.length === 0 ? <EmptyState compact title="No sent issues yet" description="Activity appears after an issue is sent." />
53
+ : res.error ? <ApiError error={res.error} title="Could not load the activity" onRetry={() => void res.refetch()} />
54
+ : res.loading && !res.data ? <Skeleton className="h-16 w-full" aria-busy="true" />
55
+ : events.length === 0 ? <EmptyState compact title="No activity recorded" description="This person has not opened or clicked this issue." />
56
+ : (
57
+ <ol className="grid gap-2">
58
+ {events.map((e, i) => (
59
+ <li key={i} className="rounded-md border p-2 text-sm"><span className="font-medium">{str(e.type) || str(e.event) || "Event"}</span> <span className="text-muted-foreground text-xs">{when(e.occurred_at) || when(e.timestamp) || when(e.created_at)}</span>{str(e.url) ? <span className="text-muted-foreground block truncate text-xs">{str(e.url)}</span> : null}</li>
60
+ ))}
61
+ </ol>
62
+ )}
63
+ </SectionCard>
64
+ );
65
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Small helpers for the newsletter management components (settings, versions, list syncs, templates).
5
+ * Responses for these endpoints are open dicts in the contract, so every reader accepts what is present.
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
+ /** The rows of a list payload: a bare array, or the first of `keys` (then `items`) that holds one. */
24
+ export function rowsOf<T = Rec>(data: unknown, ...keys: string[]): T[] {
25
+ if (Array.isArray(data)) return data as T[];
26
+ const rec = asRec(data);
27
+ for (const key of [...keys, "items"]) if (Array.isArray(rec[key])) return rec[key] as T[];
28
+ return [];
29
+ }
30
+
31
+ /** The fields of `draft` that differ from `saved`: the body of a partial update. Strings are compared trimmed. */
32
+ export function changedFields<T extends Record<string, string | boolean>>(saved: T, draft: T): Partial<T> {
33
+ const out: Partial<T> = {};
34
+ for (const key of Object.keys(draft) as Array<keyof T>) {
35
+ const a = saved[key];
36
+ const b = draft[key];
37
+ if (typeof b === "string" ? b.trim() !== String(a).trim() : b !== a) out[key] = (typeof b === "string" ? b.trim() : b) as T[keyof T];
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** True for a bare hostname such as updates.acme.com (no scheme, path or spaces). */
43
+ export function isDomain(raw: string): boolean {
44
+ return /^(?=.{4,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(raw.trim());
45
+ }
46
+
47
+ /** The DNS records a verify-domain answer lists, under whichever key it used. */
48
+ export function dnsRecordsOf(data: unknown): Rec[] {
49
+ return rowsOf<Rec>(data, "dns_records", "records");
50
+ }
51
+
52
+ /** A version row's number, under whichever key it used. */
53
+ export function versionNumber(v: Rec): number | undefined {
54
+ return num(v.version_num) ?? num(v.version) ?? num(v.number) ?? num(v.id);
55
+ }
56
+
57
+ /** Wrap rows and the API's "next" marker (a cursor or an offset; null or absent when there is no more) for `usePaginated`. */
58
+ export function pageOf<T>(rows: T[], next: unknown): PaginatedResponse<T> {
59
+ return { data: rows, pagination: { next_cursor: next == null || next === "" ? null : String(next) } } as unknown as PaginatedResponse<T>;
60
+ }
@@ -14,27 +14,40 @@
14
14
  "lists"
15
15
  ],
16
16
  "operations": [
17
- "list_nurtures_nurtures_get",
18
- "pause_nurture_nurtures__nurture_id__pause_post",
19
- "resume_nurture_nurtures__nurture_id__resume_post",
20
- "duplicate_nurture_nurtures__nurture_id__duplicate_post",
21
- "delete_nurture_nurtures__nurture_id__delete",
22
- "list_my_mailboxes_mailboxes_my_get",
23
- "list_lists_lists_get",
17
+ "add_contacts_to_list_lists__list_id__contacts_post",
24
18
  "create_nurture_nurtures_post",
25
- "update_nurture_nurtures__nurture_id__patch",
19
+ "delete_nurture_nurtures__nurture_id__delete",
20
+ "duplicate_nurture_nurtures__nurture_id__duplicate_post",
26
21
  "get_nurture_nurtures__nurture_id__get",
27
- "add_contacts_to_list_lists__list_id__contacts_post",
22
+ "get_sequence_performance_by_id_analytics_outbound_sequence_performance__sequence_id__get",
28
23
  "launch_nurture_nurtures__nurture_id__launch_post",
29
- "get_sequence_performance_by_id_analytics_outbound_sequence_performance__sequence_id__get"
24
+ "list_lists_lists_get",
25
+ "list_my_mailboxes_mailboxes_my_get",
26
+ "list_nurtures_nurtures_get",
27
+ "pause_nurture_nurtures__nurture_id__pause_post",
28
+ "resume_nurture_nurtures__nurture_id__resume_post",
29
+ "update_nurture_nurtures__nurture_id__patch"
30
30
  ],
31
31
  "components": [
32
32
  {
33
33
  "name": "NurtureScreen",
34
34
  "file": "components/nurture-screen.tsx",
35
35
  "purpose": "Finished nurture workspace: list with state and row actions, create sheet, detail with results, audience and launch, and settings.",
36
- "operations": [],
37
- "tiers": []
36
+ "operations": [
37
+ "delete_nurture_nurtures__nurture_id__delete",
38
+ "duplicate_nurture_nurtures__nurture_id__duplicate_post",
39
+ "get_nurture_nurtures__nurture_id__get",
40
+ "list_lists_lists_get",
41
+ "list_my_mailboxes_mailboxes_my_get",
42
+ "list_nurtures_nurtures_get",
43
+ "pause_nurture_nurtures__nurture_id__pause_post",
44
+ "resume_nurture_nurtures__nurture_id__resume_post"
45
+ ],
46
+ "tiers": [
47
+ "read",
48
+ "write",
49
+ "destructive"
50
+ ]
38
51
  },
39
52
  {
40
53
  "name": "NurtureBuilder",
@@ -58,9 +71,10 @@
58
71
  "file": "components/nurture-form.tsx",
59
72
  "purpose": "Create a nurture (name, sender mailbox, audience, email steps) or edit its settings.",
60
73
  "operations": [
61
- "list_my_mailboxes_mailboxes_my_get",
62
- "list_lists_lists_get",
63
74
  "create_nurture_nurtures_post",
75
+ "get_nurture_nurtures__nurture_id__get",
76
+ "list_lists_lists_get",
77
+ "list_my_mailboxes_mailboxes_my_get",
64
78
  "update_nurture_nurtures__nurture_id__patch"
65
79
  ],
66
80
  "tiers": [
@@ -17,6 +17,9 @@
17
17
  import { useState } from "react";
18
18
  import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
19
19
  import { ApiError } from "@/g8/components/api-error";
20
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
21
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
22
+ import { LoadMore } from "@/g8/components/paging-notices";
20
23
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
24
  import { EmptyState } from "@/g8/components/empty-state";
22
25
  import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
@@ -25,6 +28,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
28
  import { isFinished, isLiveLike, isNurtureOff, isPaused, statusOf } from "../lib/nurture-types";
26
29
  import type { Nurture } from "../lib/nurture-types";
27
30
  import { NurtureForm } from "./nurture-form";
31
+ import { useBrand } from "@/g8/hooks/use-brand";
28
32
 
29
33
  export interface NurtureBuilderProps {
30
34
  /** Called when a nurture is chosen for launch/enrollment (render NurtureEnroll with its id). */
@@ -35,10 +39,15 @@ export interface NurtureBuilderProps {
35
39
  type Action = { kind: "pause" | "duplicate"; nurture: Nurture };
36
40
 
37
41
  export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilderProps) {
42
+ const brand = useBrand();
38
43
  const [form, setForm] = useState<{ nurture?: Nurture } | null>(null);
39
44
  const [toArchive, setToArchive] = useState<Nurture | null>(null);
40
45
  const [toResume, setToResume] = useState<Nurture | null>(null);
41
- const list = useG8Query((signal) => g8Get<{ nurtures?: Nurture[] }>("/nurtures", { page: 1, limit: pageSize }, { signal }).then((r) => r.data?.nurtures ?? []), [pageSize]);
46
+ const list = useLoadMoreList(byPage<Nurture>(pageSize, async (page, signal) => {
47
+ const r = await g8Get<{ nurtures?: Nurture[] }>("/nurtures", { page, limit: pageSize }, { signal });
48
+ const rows = r.data?.nurtures ?? [];
49
+ return { rows, hasNext: hasNextOf(r, pageSize, rows.length) };
50
+ }), [pageSize]);
42
51
  const [busy, setBusy] = useState<string | null>(null);
43
52
  const act = useG8Mutation((a: Action) => g8Post(`/nurtures/${encodeURIComponent(a.nurture.id)}/${a.kind}`, a.kind === "duplicate" ? {} : undefined), { onSuccess: () => void list.refetch() });
44
53
  const run = async (a: Action): Promise<void> => {
@@ -49,7 +58,7 @@ export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilde
49
58
  const resume = useG8Mutation((n: Nurture) => g8Post(`/nurtures/${encodeURIComponent(n.id)}/resume`), { onSuccess: () => { setToResume(null); void list.refetch(); } });
50
59
  const archive = useG8Mutation((n: Nurture) => g8Delete(`/nurtures/${encodeURIComponent(n.id)}`), { onSuccess: () => { setToArchive(null); void list.refetch(); } });
51
60
 
52
- if (isNurtureOff(list.error)) return <EmptyState title="Nurture is not enabled" description="Nurture is behind a feature flag for this workspace. Ask graph8 to turn it on, then reload." />;
61
+ if (isNurtureOff(list.error)) return <EmptyState title="Nurture is not enabled" description={`Nurture is behind a feature flag for this workspace. Ask ${brand.linkLabel} to turn it on, then reload.`} />;
53
62
  if (list.error) return <ApiError error={list.error} title="Could not load nurtures" onRetry={() => void list.refetch()} />;
54
63
  if (form) {
55
64
  return (
@@ -63,7 +72,7 @@ export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilde
63
72
  return (
64
73
  <div className="grid gap-3">
65
74
  <div className="flex items-center justify-between">
66
- <h2 className="text-lg font-semibold">Nurtures</h2>
75
+ <h2 className="text-lg font-medium">Nurtures</h2>
67
76
  <Button onClick={() => setForm({})}>New nurture</Button>
68
77
  </div>
69
78
  {act.error ? <ApiError error={act.error} title="That did not work" /> : null}
@@ -89,6 +98,7 @@ export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilde
89
98
  ))}
90
99
  </ul>
91
100
  )}
101
+ <LoadMore shown={nurtures.length} noun="nurtures" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
92
102
  <ConfirmSpendDialog
93
103
  open={toResume !== null}
94
104
  tier="external"
@@ -17,13 +17,17 @@
17
17
  import { useEffect, useState } from "react";
18
18
  import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
19
19
  import { ApiError } from "@/g8/components/api-error";
20
+ import { useAllLists } from "@/g8/crm/hooks/use-all-lists";
21
+ import { ListSelect } from "@/g8/crm/components/list-select";
20
22
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
23
  import { EmptyState } from "@/g8/components/empty-state";
22
24
  import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
23
25
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
26
  import { useG8Query } from "@/g8/hooks/use-g8-query";
25
27
  import { isDrafted, isNurtureMissing, isNurtureOff, statusOf } from "../lib/nurture-types";
26
- import type { ListOption, Nurture } from "../lib/nurture-types";
28
+ import type { Nurture } from "../lib/nurture-types";
29
+ import { pluralWord } from "@/g8/lib/plural";
30
+ import { useBrand } from "@/g8/hooks/use-brand";
27
31
 
28
32
  export interface NurtureEnrollProps {
29
33
  nurtureId: string;
@@ -33,9 +37,10 @@ export interface NurtureEnrollProps {
33
37
  }
34
38
 
35
39
  export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: NurtureEnrollProps) {
40
+ const brand = useBrand();
36
41
  const base = `/nurtures/${encodeURIComponent(nurtureId)}`;
37
42
  const nurture = useG8Query((signal) => g8Get<Nurture>(base, {}, { signal }).then((r) => r.data), [base]);
38
- const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
43
+ const lists = useAllLists([]);
39
44
  const [listId, setListId] = useState("");
40
45
  const [consent, setConsent] = useState(false);
41
46
  const [confirming, setConfirming] = useState(false);
@@ -55,12 +60,12 @@ export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: Nurtur
55
60
  const addContacts = useG8Mutation((id: number) => g8Post(`/lists/${id}/contacts`, { contact_ids: [...contactIds] }), { onSuccess: () => void lists.refetch() });
56
61
  const launch = useG8Mutation(() => g8Post(`${base}/launch`), { onSuccess: () => { setConfirming(false); void nurture.refetch(); onLaunched?.(); } });
57
62
 
58
- if (isNurtureOff(nurture.error)) return <EmptyState title="Nurture is not enabled" description="Nurture is behind a feature flag for this workspace. Ask graph8 to turn it on." />;
63
+ if (isNurtureOff(nurture.error)) return <EmptyState title="Nurture is not enabled" description={`Nurture is behind a feature flag for this workspace. Ask ${brand.linkLabel} to turn it on.`} />;
59
64
  if (isNurtureMissing(nurture.error)) return <EmptyState title="This nurture no longer exists" description="It may have been deleted, or it is not yours to see. Pick another nurture." />;
60
65
  if (nurture.error) return <ApiError error={nurture.error} title="Could not load the nurture" onRetry={() => void nurture.refetch()} />;
61
66
  if (!nurture.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
62
67
 
63
- const list = (lists.data ?? []).find((l) => String(l.id) === listId);
68
+ const list = lists.items.find((l) => String(l.id) === listId);
64
69
  const attached = nurture.data.associated_list_id ? String(nurture.data.associated_list_id) === listId : false;
65
70
  const audience = list?.contact_count ?? 0;
66
71
  const canLaunch = isDrafted(nurture.data) && attached && audience > 0 && consent;
@@ -70,15 +75,12 @@ export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: Nurtur
70
75
  <CardContent className="grid gap-4">
71
76
  <div className="grid max-w-md gap-1">
72
77
  <label htmlFor="g8-nurture-audience" className="text-sm font-medium">Audience list</label>
73
- <select id="g8-nurture-audience" className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={listId} disabled={!isDrafted(nurture.data)} onChange={(e) => setListId(e.target.value)}>
74
- <option value="">Choose a list</option>
75
- {(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title} ({(l.contact_count ?? 0).toLocaleString()})</option>)}
76
- </select>
78
+ <ListSelect id="g8-nurture-audience" label="Audience list" emptyOption="Choose a list" pageSize={200} optionLabel={(l) => `${l.title} (${(l.contact_count ?? 0).toLocaleString()})`} value={listId} valueLabel={list?.title} disabled={!isDrafted(nurture.data)} onChange={(l) => setListId(l ? String(l.id) : "")} />
77
79
  <div className="flex gap-2">
78
80
  <Button size="sm" variant="outline" loading={setAudience.loading} disabled={!listId || attached || !isDrafted(nurture.data)} onClick={() => void setAudience.mutate(Number(listId))}>Use this list</Button>
79
- {contactIds.length > 0 ? <Button size="sm" variant="outline" loading={addContacts.loading} disabled={!attached} onClick={() => void addContacts.mutate(Number(listId))}>Add {contactIds.length} selected {contactIds.length === 1 ? "contact" : "contacts"}</Button> : null}
81
+ {contactIds.length > 0 ? <Button size="sm" variant="outline" loading={addContacts.loading} disabled={!attached} onClick={() => void addContacts.mutate(Number(listId))}>Add {contactIds.length} selected {pluralWord(contactIds.length, "contact")}</Button> : null}
80
82
  </div>
81
- {attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {audience === 1 ? "contact" : "contacts"}.</p> : null}
83
+ {attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {pluralWord(audience, "contact")}.</p> : null}
82
84
  </div>
83
85
  {[setAudience.error, addContacts.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
84
86
  {isDrafted(nurture.data) ? (
@@ -13,13 +13,15 @@
13
13
  */
14
14
 
15
15
  import { useState } from "react";
16
- import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
16
+ import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
17
17
  import { ApiError } from "@/g8/components/api-error";
18
+ import { ListSelect } from "@/g8/crm/components/list-select";
18
19
  import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
19
20
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
21
  import { useG8Query } from "@/g8/hooks/use-g8-query";
21
22
  import { channelPayload, stepsPayload } from "../lib/nurture-types";
22
- import type { ListOption, MailboxOption, Nurture, StepDraft } from "../lib/nurture-types";
23
+ import type { MailboxOption, Nurture, StepDraft } from "../lib/nurture-types";
24
+ import { useBrand } from "@/g8/hooks/use-brand";
23
25
 
24
26
  export interface NurtureFormProps {
25
27
  /** Edit this nurture; omit to create a new one. */
@@ -30,7 +32,22 @@ export interface NurtureFormProps {
30
32
 
31
33
  const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
32
34
 
35
+ /**
36
+ * Edit mode reads the FULL nurture (GET /nurtures/{id}) before it shows anything: the row a list hands over may omit
37
+ * fields (description, audience, flags), and saving a form seeded from it would blank them. Create mode needs no record.
38
+ */
33
39
  export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
40
+ const full = useG8Query((signal) => g8Get<Nurture>(`/nurtures/${encodeURIComponent(nurture!.id)}`, {}, { signal }).then((r) => r.data), [nurture?.id], { enabled: Boolean(nurture) });
41
+ if (nurture) {
42
+ if (full.error && (!full.data || full.data.id !== nurture.id)) return <ApiError error={full.error} title="Could not load this nurture" onRetry={() => void full.refetch()} />;
43
+ if (!full.data || full.data.id !== nurture.id) return <Skeleton className="h-40 w-full" aria-label="Loading nurture" />;
44
+ return <NurtureFormFields key={full.data.id} nurture={full.data} onSaved={onSaved} onCancel={onCancel} />;
45
+ }
46
+ return <NurtureFormFields onSaved={onSaved} onCancel={onCancel} />;
47
+ }
48
+
49
+ function NurtureFormFields({ nurture, onSaved, onCancel }: NurtureFormProps) {
50
+ const brand = useBrand();
34
51
  const editing = Boolean(nurture);
35
52
  const [name, setName] = useState(nurture?.name ?? "");
36
53
  const [description, setDescription] = useState(nurture?.description ?? "");
@@ -41,7 +58,6 @@ export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
41
58
  const [unsubscribe, setUnsubscribe] = useState(nurture?.include_unsubscribe_cta ?? true);
42
59
  const [steps, setSteps] = useState<StepDraft[]>([{ waitDays: 0, subject: "", body: "" }]);
43
60
  const mailboxes = useG8Query((signal) => g8Get<{ mailboxes?: MailboxOption[] }>("/mailboxes/my", {}, { signal }).then((r) => (r.data?.mailboxes ?? []).filter((m) => !m.is_archived)), []);
44
- const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
45
61
 
46
62
  const mailbox = (mailboxes.data ?? []).find((m) => String(m.id) === mailboxId);
47
63
  const stepsOk = steps.length > 0 && steps.every((s) => s.subject.trim() && s.body.trim());
@@ -50,7 +66,11 @@ export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
50
66
  const save = useG8Mutation(async () => {
51
67
  const flags = { finish_on_reply: finishOnReply, send_in_same_thread: sameThread, include_unsubscribe_cta: unsubscribe };
52
68
  if (nurture) {
53
- return (await g8Patch<Nurture>(`/nurtures/${encodeURIComponent(nurture.id)}`, { name: name.trim(), description: description.trim() || null, associated_list_id: listId ? Number(listId) : null, ...flags })).data;
69
+ // Only what the person changed is sent.
70
+ const next = { name: name.trim(), description: description.trim() || null, associated_list_id: listId ? Number(listId) : null, ...flags };
71
+ const before = { name: nurture.name, description: nurture.description?.trim() || null, associated_list_id: nurture.associated_list_id ? Number(nurture.associated_list_id) : null, finish_on_reply: nurture.finish_on_reply ?? true, send_in_same_thread: nurture.send_in_same_thread ?? true, include_unsubscribe_cta: nurture.include_unsubscribe_cta ?? true };
72
+ const diff = Object.fromEntries(Object.entries(next).filter(([k, v]) => v !== before[k as keyof typeof before]));
73
+ return (await g8Patch<Nurture>(`/nurtures/${encodeURIComponent(nurture.id)}`, diff)).data;
54
74
  }
55
75
  return (await g8Post<Nurture>("/nurtures", {
56
76
  name: name.trim(),
@@ -75,14 +95,11 @@ export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
75
95
  <option value="">{mailboxes.loading ? "Loading..." : "Choose a mailbox"}</option>
76
96
  {(mailboxes.data ?? []).map((m) => <option key={m.id} value={m.id}>{m.email}</option>)}
77
97
  </select>
78
- {!editing && !mailboxes.loading && (mailboxes.data ?? []).length === 0 ? <p className="mt-1 text-xs text-amber-700">No mailbox is connected. Connect one in graph8 first (Google and Microsoft sign-in happens in the app).</p> : null}
98
+ {!editing && !mailboxes.loading && (mailboxes.data ?? []).length === 0 ? <p className="mt-1 text-xs text-warning-foreground">No mailbox is connected. Connect one in {brand.linkLabel} first (Google and Microsoft sign-in happens in the app).</p> : null}
79
99
  </div>
80
100
  <div>
81
101
  <Label htmlFor="g8-nurture-list" className="mb-1">Audience list (consented contacts)</Label>
82
- <select id="g8-nurture-list" className={SELECT} value={listId} onChange={(e) => setListId(e.target.value)}>
83
- <option value="">Choose later</option>
84
- {(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
85
- </select>
102
+ <ListSelect id="g8-nurture-list" label="Audience list (consented contacts)" emptyOption="Choose later" pageSize={200} value={listId} valueLabel={nurture?.associated_list_id ? `List ${nurture.associated_list_id}` : undefined} selectClassName={SELECT} onChange={(l) => setListId(l ? String(l.id) : "")} />
86
103
  </div>
87
104
  </div>
88
105
  <fieldset className="flex flex-wrap gap-4 text-sm">
@@ -93,7 +110,7 @@ export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
93
110
  </fieldset>
94
111
  {editing ? <p className="text-muted-foreground text-sm">The emails and the sender are fixed once created. To change them, duplicate this nurture and build the copy.</p> : (
95
112
  <div className="grid gap-3">
96
- <h3 className="text-sm font-semibold">Emails</h3>
113
+ <h3 className="text-sm font-medium">Emails</h3>
97
114
  {steps.map((s, i) => (
98
115
  <div key={i} className="grid gap-2 rounded-md border p-3">
99
116
  <div className="flex items-end gap-2">
@@ -25,6 +25,10 @@ import type { ReactNode } from "react";
25
25
  import { Copy, ExternalLink, HeartHandshake, Pause, Play, Plus, Trash2 } from "lucide-react";
26
26
  import { Button, Skeleton } from "@graph8/sdk/ui";
27
27
  import { ApiError } from "@/g8/components/api-error";
28
+ import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
29
+ import { byPage, hasNextOf } from "@/g8/lib/paging";
30
+ import { LoadMore } from "@/g8/components/paging-notices";
31
+ import { useAllLists } from "@/g8/crm/hooks/use-all-lists";
28
32
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
33
  import { EmptyState } from "@/g8/components/empty-state";
30
34
  import { DetailPanel } from "@/g8/components/screen/detail-panel";
@@ -41,10 +45,12 @@ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
41
45
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
42
46
  import { useG8Query } from "@/g8/hooks/use-g8-query";
43
47
  import { isDrafted, isFinished, isLiveLike, isNurtureOff, isPaused, statusOf } from "../lib/nurture-types";
44
- import type { ListOption, MailboxOption, Nurture } from "../lib/nurture-types";
48
+ import type { MailboxOption, Nurture } from "../lib/nurture-types";
45
49
  import { NurtureEnroll } from "./nurture-enroll";
46
50
  import { NurtureForm } from "./nurture-form";
47
51
  import { NurtureStats } from "./nurture-stats";
52
+ import { plural } from "@/g8/lib/plural";
53
+ import { useBrand } from "@/g8/hooks/use-brand";
48
54
 
49
55
  export interface NurtureScreenProps {
50
56
  /** Where a person connects a mailbox (Google and Microsoft sign-in only works in the graph8 app). */
@@ -77,6 +83,9 @@ function matchesFilter(n: Nurture, f: string): boolean {
77
83
  return isFinished(n);
78
84
  }
79
85
 
86
+ /** Nurtures per read; "Load more" appends the next page. */
87
+ const NURTURE_PAGE = 100;
88
+
80
89
  interface StepRow { step_order?: number; time_interval?: number; step_data?: { subject?: string; body?: string } }
81
90
 
82
91
  const when = (v: string | null | undefined): string => (v ? new Date(v).toLocaleString() : "");
@@ -85,12 +94,12 @@ const yesNo = (v: boolean | undefined, fallback: boolean): string => ((v ?? fall
85
94
  /** Every field of one nurture, its audience and sender by name, and each email of the series. */
86
95
  function NurtureDetails({ nurtureId }: { nurtureId: string }) {
87
96
  const one = useG8Query((signal) => g8Get<Nurture>(`/nurtures/${encodeURIComponent(nurtureId)}`, {}, { signal }).then((r) => r.data), [nurtureId]);
88
- const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
97
+ const lists = useAllLists([]);
89
98
  const boxes = useG8Query((signal) => g8Get<{ mailboxes?: MailboxOption[] }>("/mailboxes/my", {}, { signal }).then((r) => r.data?.mailboxes ?? []), []);
90
99
  if (one.error) return <ApiError error={one.error} title="Could not load the nurture" onRetry={() => void one.refetch()} />;
91
100
  if (!one.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
92
101
  const n = one.data;
93
- const list = (lists.data ?? []).find((l) => l.id === n.associated_list_id);
102
+ const list = lists.items.find((l) => l.id === n.associated_list_id);
94
103
  const box = (boxes.data ?? []).find((m) => m.id === n.pinned_mailbox_id);
95
104
  const steps = Array.isArray(n.steps) ? (n.steps as StepRow[]) : [];
96
105
  return (
@@ -129,10 +138,12 @@ function NurtureDetails({ nurtureId }: { nurtureId: string }) {
129
138
 
130
139
  /** "Wait 1 day" / "Wait 3 days" after the previous email (`time_interval` is seconds). */
131
140
  function waitText(days: number): string {
132
- return `Wait ${days} ${days === 1 ? "day" : "days"} after the previous email`;
141
+ return `Wait ${plural(days, "day")} after the previous email`;
133
142
  }
134
143
 
135
- export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", contactIds, renderRowActions, renderDetailActions, renderAudienceTools, extraDetailTabs, toolbarActions, className }: NurtureScreenProps) {
144
+ export function NurtureScreen({ connectMailboxUrl: connectMailboxUrlProp, contactIds, renderRowActions, renderDetailActions, renderAudienceTools, extraDetailTabs, toolbarActions, className }: NurtureScreenProps) {
145
+ const brand = useBrand();
146
+ const connectMailboxUrl = connectMailboxUrlProp ?? brand.appUrl;
136
147
  const [id, setId] = useUrlState("id", "");
137
148
  const [view, setView] = useUrlState("view", "");
138
149
  const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
@@ -143,7 +154,11 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
143
154
  const [toArchive, setToArchive] = useState<Nurture | null>(null);
144
155
  const [busy, setBusy] = useState<string | null>(null);
145
156
 
146
- const list = useG8Query((signal) => g8Get<{ nurtures?: Nurture[] }>("/nurtures", { page: 1, limit: 100 }, { signal }).then((r) => r.data?.nurtures ?? []), []);
157
+ const list = useLoadMoreList(byPage<Nurture>(NURTURE_PAGE, async (page, signal) => {
158
+ const r = await g8Get<{ nurtures?: Nurture[] }>("/nurtures", { page, limit: NURTURE_PAGE }, { signal });
159
+ const rows = r.data?.nurtures ?? [];
160
+ return { rows, hasNext: hasNextOf(r, NURTURE_PAGE, rows.length) };
161
+ }), []);
147
162
  const mailboxes = useG8Query((signal) => g8Get<{ mailboxes?: MailboxOption[] }>("/mailboxes/my", {}, { signal }).then((r) => (r.data?.mailboxes ?? []).filter((m) => !m.is_archived)), [], { enabled: creating });
148
163
 
149
164
  const act = useG8Mutation((a: { kind: "pause" | "duplicate"; nurture: Nurture }) => g8Post(`/nurtures/${encodeURIComponent(a.nurture.id)}/${a.kind}`, a.kind === "duplicate" ? {} : undefined), { onSuccess: () => void list.refetch() });
@@ -158,7 +173,7 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
158
173
  });
159
174
 
160
175
  if (isNurtureOff(list.error)) {
161
- return <ScreenLayout title="Nurture" description="Lifecycle email series for people who agreed to hear from you." icon={<HeartHandshake className="size-5" />}><EmptyState title="Nurture is not enabled" description="Nurture is switched off for this workspace. Ask graph8 to turn it on, then reload." /></ScreenLayout>;
176
+ return <ScreenLayout title="Nurture" description="Lifecycle email series for people who agreed to hear from you." icon={<HeartHandshake className="size-5" />}><EmptyState title="Nurture is not enabled" description={`Nurture is switched off for this workspace. Ask ${brand.linkLabel} to turn it on, then reload.`} /></ScreenLayout>;
162
177
  }
163
178
 
164
179
  const all = list.data ?? [];
@@ -177,7 +192,7 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
177
192
  <li key={n.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${n.id === id ? "bg-muted" : ""}`}>
178
193
  <button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(n.id)}>
179
194
  <span className="block truncate font-medium">{n.name}</span>
180
- <span className="text-muted-foreground block text-xs">{Array.isArray(n.steps) && n.steps.length > 0 ? `${n.steps.length} ${n.steps.length === 1 ? "email" : "emails"}` : "Series"}{n.is_shared ? " · shared" : ""}</span>
195
+ <span className="text-muted-foreground block text-xs">{Array.isArray(n.steps) && n.steps.length > 0 ? plural(n.steps.length, "email") : "Series"}{n.is_shared ? " · shared" : ""}</span>
181
196
  </button>
182
197
  <StatusPill tone={toneOf(n)}>{statusOf(n)}</StatusPill>
183
198
  <div className="flex gap-1">
@@ -243,12 +258,12 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
243
258
  )}
244
259
  >
245
260
  {act.error ? <ApiError error={act.error} title="That did not work" /> : null}
246
- <SplitView list={rows} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => { setId(null); setView(null); }} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)} detailWidth="xl" />
261
+ <SplitView list={<>{rows}<LoadMore shown={all.length} noun="nurtures" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} /></>} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => { setId(null); setView(null); }} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)} detailWidth="xl" />
247
262
  <FormSheet open={creating} onOpenChange={setCreating} title="New nurture" description="Pick the sender and the audience, then write the emails. Nothing is sent until you go live.">
248
263
  <div className="grid gap-4">
249
264
  {noMailbox ? (
250
- <SectionCard title="Connect a mailbox first" description="A nurture sends from one connected mailbox. Google and Microsoft sign-in only works inside graph8.">
251
- <a className="inline-flex items-center gap-1 text-sm underline" href={connectMailboxUrl} target="_blank" rel="noreferrer">Connect in graph8 <ExternalLink className="size-3.5" /></a>
265
+ <SectionCard title="Connect a mailbox first" description={`A nurture sends from one connected mailbox. Google and Microsoft sign-in only works inside ${brand.linkLabel}.`}>
266
+ <a className="inline-flex items-center gap-1 text-sm underline" href={connectMailboxUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel} <ExternalLink className="size-3.5" /></a>
252
267
  </SectionCard>
253
268
  ) : null}
254
269
  {creating ? <NurtureForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); void list.refetch(); setId(created.id); }} /> : null}
@@ -15,11 +15,12 @@
15
15
  import { useState } from "react";
16
16
  import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
17
17
  import { ApiError } from "@/g8/components/api-error";
18
+ import { useAllLists } from "@/g8/crm/hooks/use-all-lists";
18
19
  import { EmptyState } from "@/g8/components/empty-state";
19
20
  import { g8Get } from "@/g8/lib/g8-client";
20
21
  import { useG8Query } from "@/g8/hooks/use-g8-query";
21
22
  import { isNurtureMissing, isNurtureOff, percentOf, statusOf } from "../lib/nurture-types";
22
- import type { ListOption, Nurture, NurturePerformance } from "../lib/nurture-types";
23
+ import type { Nurture, NurturePerformance } from "../lib/nurture-types";
23
24
 
24
25
  export interface NurtureStatsProps {
25
26
  nurtureId: string;
@@ -31,7 +32,7 @@ function Tile({ label, value }: { label: string; value: string }) {
31
32
  return (
32
33
  <div className="rounded-md border p-3">
33
34
  <p className="text-muted-foreground text-xs">{label}</p>
34
- <p className="text-xl font-semibold">{value}</p>
35
+ <p className="text-xl font-medium">{value}</p>
35
36
  </div>
36
37
  );
37
38
  }
@@ -39,7 +40,7 @@ function Tile({ label, value }: { label: string; value: string }) {
39
40
  export function NurtureStats({ nurtureId, days = 30 }: NurtureStatsProps) {
40
41
  const [windowDays, setWindowDays] = useState(days);
41
42
  const nurture = useG8Query((signal) => g8Get<Nurture>(`/nurtures/${encodeURIComponent(nurtureId)}`, {}, { signal }).then((r) => r.data), [nurtureId]);
42
- const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
43
+ const lists = useAllLists([]);
43
44
  const perf = useG8Query(
44
45
  (signal) => g8Get<{ data?: NurturePerformance }>(`/analytics/outbound/sequence-performance/${encodeURIComponent(nurtureId)}`, { days: windowDays }, { signal }).then((r) => (r.data?.data ?? (r.data as NurturePerformance | undefined)) ?? null),
45
46
  [nurtureId, windowDays],
@@ -49,7 +50,7 @@ export function NurtureStats({ nurtureId, days = 30 }: NurtureStatsProps) {
49
50
  if (isNurtureMissing(nurture.error)) return <EmptyState title="This nurture no longer exists" description="It may have been deleted, or it is not yours to see. Pick another nurture." />;
50
51
  if (nurture.error) return <ApiError error={nurture.error} title="Could not load the nurture" onRetry={() => void nurture.refetch()} />;
51
52
  if (!nurture.data) return <Skeleton className="h-32 w-full" aria-busy="true" />;
52
- const audience = (lists.data ?? []).find((l) => l.id === nurture.data?.associated_list_id);
53
+ const audience = lists.items.find((l) => l.id === nurture.data?.associated_list_id);
53
54
  const p = perf.data;
54
55
  const hasNumbers = Boolean(p && (p.contacts_total || p.emails_sent));
55
56
  return (
@@ -8,7 +8,7 @@
8
8
  * endpoint; status, audience size and the sequence-performance report are what the API gives.
9
9
  */
10
10
 
11
- import { G8Error } from "@graph8/sdk";
11
+ import { G8Error } from "@graph8/sdk/errors";
12
12
 
13
13
  export interface Nurture {
14
14
  id: string;
@@ -5,6 +5,22 @@ edit and archive their records. `ObjectTable` and `ObjectRecordDetail` are gener
5
5
  slug, including the standard `contacts`, `companies` and `deals`. This is the HubSpot "custom objects" / Salesforce
6
6
  "custom object" screen set.
7
7
 
8
+ ## Screens
9
+
10
+ `ObjectsScreen` (no props) is the finished workspace: a picker of all objects (your own next to the standard contacts, companies and
11
+ deals), the records table with search, load more, create, edit and archive, the open record beside it (values, owners, relationships,
12
+ history, restore) and "Define objects", which opens `ObjectSchemaEditor` in a side sheet so an admin can create objects, add and archive
13
+ fields and rename. The URL carries `?object=` and `?id=`, so a refresh or a shared link lands on the same record. Props:
14
+ `initialObjectSlug`, `allowSchemaEditing` (default true; hide it from non-admins), `toolbarActions`. Needs the same prerequisites as
15
+ the parts (`objects` and `records` in the proxy allow-list, `objects:read`/`write`/`delete`, and `objects:manage` for the editor).
16
+ If the objects API is unreachable the screen says so.
17
+
18
+ ```tsx
19
+ "use client";
20
+ import { ObjectsScreen } from "@/g8/objects/components/objects-screen";
21
+ export default function Page() { return <ObjectsScreen />; }
22
+ ```
23
+
8
24
  ## Install
9
25
 
10
26
  ```
@@ -35,7 +51,7 @@ Installs `foundation` first if it is missing. Everything lands under `g8/objects
35
51
  5. **Archive**: the only delete. The record leaves lists but stays readable by id with its history; `restoreObjectRecord` brings it back.
36
52
  Archiving a record, a field or a whole object always goes through `ConfirmSpendDialog`.
37
53
 
38
- For `contacts`, `companies` and `deals`, `ObjectRecordDetail` also renders `RecordRolesPanel` (SDR / AE / CSM owner slots).
54
+ For `contacts`, `companies` and `deals`, `ObjectRecordDetail` also renders `RecordRolesPanel` (SDR / AE / CSM owner slots; each slot is a teammate picker, so nobody types a user id; `userOptions` still gives a fixed list). A person attribute (`actor_reference`) is a teammate picker too.
39
55
  Custom objects have no role slots.
40
56
 
41
57
  ```tsx
@@ -82,6 +98,7 @@ export default function Invoices() {
82
98
 
83
99
  | Component | File | What it is |
84
100
  |---|---|---|
101
+ | `ObjectsScreen` | `components/objects-screen.tsx` | The finished workspace composing everything below. |
85
102
  | `ObjectSchemaEditor` | `components/object-schema-editor.tsx` | Create objects, edit their labels, add/edit/archive fields (text, number, select, reference...). |
86
103
  | `AttributeFormDialog` | `components/attribute-form-dialog.tsx` | Dialog to add or edit one field of an object. |
87
104
  | `ObjectTable` | `components/object-table.tsx` | Records of any object as a table with search over loaded rows, load-more, create, edit and archive. |