@graph8/sdk 0.361.0 → 0.370.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 (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -14,21 +14,41 @@
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
+ {
33
+ "name": "NurtureScreen",
34
+ "file": "components/nurture-screen.tsx",
35
+ "purpose": "Finished nurture workspace: list with state and row actions, create sheet, detail with results, audience and launch, and settings.",
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
+ ]
51
+ },
32
52
  {
33
53
  "name": "NurtureBuilder",
34
54
  "file": "components/nurture-builder.tsx",
@@ -51,9 +71,10 @@
51
71
  "file": "components/nurture-form.tsx",
52
72
  "purpose": "Create a nurture (name, sender mailbox, audience, email steps) or edit its settings.",
53
73
  "operations": [
54
- "list_my_mailboxes_mailboxes_my_get",
55
- "list_lists_lists_get",
56
74
  "create_nurture_nurtures_post",
75
+ "get_nurture_nurtures__nurture_id__get",
76
+ "list_lists_lists_get",
77
+ "list_my_mailboxes_mailboxes_my_get",
57
78
  "update_nurture_nurtures__nurture_id__patch"
58
79
  ],
59
80
  "tiers": [
@@ -186,6 +207,7 @@
186
207
  }
187
208
  ],
188
209
  "peerDependencies": {
189
- "react": ">=18"
210
+ "react": ">=18",
211
+ "lucide-react": ">=0.400.0"
190
212
  }
191
213
  }
@@ -25,6 +25,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
25
  import { isFinished, isLiveLike, isNurtureOff, isPaused, statusOf } from "../lib/nurture-types";
26
26
  import type { Nurture } from "../lib/nurture-types";
27
27
  import { NurtureForm } from "./nurture-form";
28
+ import { useBrand } from "@/g8/hooks/use-brand";
28
29
 
29
30
  export interface NurtureBuilderProps {
30
31
  /** Called when a nurture is chosen for launch/enrollment (render NurtureEnroll with its id). */
@@ -35,6 +36,7 @@ export interface NurtureBuilderProps {
35
36
  type Action = { kind: "pause" | "duplicate"; nurture: Nurture };
36
37
 
37
38
  export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilderProps) {
39
+ const brand = useBrand();
38
40
  const [form, setForm] = useState<{ nurture?: Nurture } | null>(null);
39
41
  const [toArchive, setToArchive] = useState<Nurture | null>(null);
40
42
  const [toResume, setToResume] = useState<Nurture | null>(null);
@@ -49,7 +51,7 @@ export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilde
49
51
  const resume = useG8Mutation((n: Nurture) => g8Post(`/nurtures/${encodeURIComponent(n.id)}/resume`), { onSuccess: () => { setToResume(null); void list.refetch(); } });
50
52
  const archive = useG8Mutation((n: Nurture) => g8Delete(`/nurtures/${encodeURIComponent(n.id)}`), { onSuccess: () => { setToArchive(null); void list.refetch(); } });
51
53
 
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." />;
54
+ 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
55
  if (list.error) return <ApiError error={list.error} title="Could not load nurtures" onRetry={() => void list.refetch()} />;
54
56
  if (form) {
55
57
  return (
@@ -24,6 +24,8 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
24
  import { useG8Query } from "@/g8/hooks/use-g8-query";
25
25
  import { isDrafted, isNurtureMissing, isNurtureOff, statusOf } from "../lib/nurture-types";
26
26
  import type { ListOption, Nurture } from "../lib/nurture-types";
27
+ import { pluralWord } from "@/g8/lib/plural";
28
+ import { useBrand } from "@/g8/hooks/use-brand";
27
29
 
28
30
  export interface NurtureEnrollProps {
29
31
  nurtureId: string;
@@ -33,6 +35,7 @@ export interface NurtureEnrollProps {
33
35
  }
34
36
 
35
37
  export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: NurtureEnrollProps) {
38
+ const brand = useBrand();
36
39
  const base = `/nurtures/${encodeURIComponent(nurtureId)}`;
37
40
  const nurture = useG8Query((signal) => g8Get<Nurture>(base, {}, { signal }).then((r) => r.data), [base]);
38
41
  const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
@@ -55,7 +58,7 @@ export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: Nurtur
55
58
  const addContacts = useG8Mutation((id: number) => g8Post(`/lists/${id}/contacts`, { contact_ids: [...contactIds] }), { onSuccess: () => void lists.refetch() });
56
59
  const launch = useG8Mutation(() => g8Post(`${base}/launch`), { onSuccess: () => { setConfirming(false); void nurture.refetch(); onLaunched?.(); } });
57
60
 
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." />;
61
+ 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
62
  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
63
  if (nurture.error) return <ApiError error={nurture.error} title="Could not load the nurture" onRetry={() => void nurture.refetch()} />;
61
64
  if (!nurture.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
@@ -76,9 +79,9 @@ export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: Nurtur
76
79
  </select>
77
80
  <div className="flex gap-2">
78
81
  <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}
82
+ {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
83
  </div>
81
- {attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {audience === 1 ? "contact" : "contacts"}.</p> : null}
84
+ {attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {pluralWord(audience, "contact")}.</p> : null}
82
85
  </div>
83
86
  {[setAudience.error, addContacts.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
84
87
  {isDrafted(nurture.data) ? (
@@ -13,13 +13,14 @@
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
18
  import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
19
19
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
20
  import { useG8Query } from "@/g8/hooks/use-g8-query";
21
21
  import { channelPayload, stepsPayload } from "../lib/nurture-types";
22
22
  import type { ListOption, MailboxOption, Nurture, StepDraft } from "../lib/nurture-types";
23
+ import { useBrand } from "@/g8/hooks/use-brand";
23
24
 
24
25
  export interface NurtureFormProps {
25
26
  /** Edit this nurture; omit to create a new one. */
@@ -30,7 +31,22 @@ export interface NurtureFormProps {
30
31
 
31
32
  const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
32
33
 
34
+ /**
35
+ * Edit mode reads the FULL nurture (GET /nurtures/{id}) before it shows anything: the row a list hands over may omit
36
+ * fields (description, audience, flags), and saving a form seeded from it would blank them. Create mode needs no record.
37
+ */
33
38
  export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
39
+ const full = useG8Query((signal) => g8Get<Nurture>(`/nurtures/${encodeURIComponent(nurture!.id)}`, {}, { signal }).then((r) => r.data), [nurture?.id], { enabled: Boolean(nurture) });
40
+ if (nurture) {
41
+ 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()} />;
42
+ if (!full.data || full.data.id !== nurture.id) return <Skeleton className="h-40 w-full" aria-label="Loading nurture" />;
43
+ return <NurtureFormFields key={full.data.id} nurture={full.data} onSaved={onSaved} onCancel={onCancel} />;
44
+ }
45
+ return <NurtureFormFields onSaved={onSaved} onCancel={onCancel} />;
46
+ }
47
+
48
+ function NurtureFormFields({ nurture, onSaved, onCancel }: NurtureFormProps) {
49
+ const brand = useBrand();
34
50
  const editing = Boolean(nurture);
35
51
  const [name, setName] = useState(nurture?.name ?? "");
36
52
  const [description, setDescription] = useState(nurture?.description ?? "");
@@ -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,7 +95,7 @@ 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-amber-700">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>
@@ -0,0 +1,283 @@
1
+ "use client";
2
+
3
+ /**
4
+ * NurtureScreen: the finished nurture workspace. No props needed.
5
+ *
6
+ * Composes: ScreenLayout (title, stats), Toolbar (search, status filter), SplitView + DetailPanel (selected
7
+ * nurture, deep link `?id=`), FormSheet (new nurture via NurtureForm), NurtureStats (overview and results),
8
+ * NurtureEnroll (audience list, consent, Go live), NurtureForm in edit mode (settings).
9
+ *
10
+ * The detail panel (`?id=`) has Overview, Details (every field and each email), Audience and launch, and Settings;
11
+ * `&view=full` shows it as a full page.
12
+ *
13
+ * User flows: see every nurture with its state, search and filter, create a nurture (sender mailbox, audience,
14
+ * email steps), open one to read its results, choose the audience and add selected contacts, tick consent and go
15
+ * live (confirmed with the audience size), pause, resume (confirmed), duplicate, edit settings and archive
16
+ * (confirmed). Connecting a mailbox needs Google or Microsoft sign-in, which only exists in the graph8 app, so
17
+ * the create sheet links out when no mailbox is connected.
18
+ *
19
+ * SDK operations: listNurtures, listMyMailboxes (only to detect "no mailbox"), pauseNurture, resumeNurture
20
+ * (external-style confirm), duplicateNurture, deleteNurture (destructive, confirmed); the rest live in the parts.
21
+ */
22
+
23
+ import { useState } from "react";
24
+ import type { ReactNode } from "react";
25
+ import { Copy, ExternalLink, HeartHandshake, Pause, Play, Plus, Trash2 } from "lucide-react";
26
+ import { Button, Skeleton } from "@graph8/sdk/ui";
27
+ import { ApiError } from "@/g8/components/api-error";
28
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
29
+ import { EmptyState } from "@/g8/components/empty-state";
30
+ import { DetailPanel } from "@/g8/components/screen/detail-panel";
31
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
32
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
33
+ import { ScreenLayout } from "@/g8/components/screen/screen-layout";
34
+ import { SectionCard } from "@/g8/components/screen/section-card";
35
+ import { SplitView } from "@/g8/components/screen/split-view";
36
+ import { StatusPill } from "@/g8/components/screen/status-pill";
37
+ import type { Tone } from "@/g8/components/screen/status-pill";
38
+ import { Toolbar } from "@/g8/components/screen/toolbar";
39
+ import { useUrlState } from "@/g8/hooks/use-url-state";
40
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
41
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
42
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
43
+ import { isDrafted, isFinished, isLiveLike, isNurtureOff, isPaused, statusOf } from "../lib/nurture-types";
44
+ import type { ListOption, MailboxOption, Nurture } from "../lib/nurture-types";
45
+ import { NurtureEnroll } from "./nurture-enroll";
46
+ import { NurtureForm } from "./nurture-form";
47
+ import { NurtureStats } from "./nurture-stats";
48
+ import { plural } from "@/g8/lib/plural";
49
+ import { useBrand } from "@/g8/hooks/use-brand";
50
+
51
+ export interface NurtureScreenProps {
52
+ /** Where a person connects a mailbox (Google and Microsoft sign-in only works in the graph8 app). */
53
+ connectMailboxUrl?: string;
54
+ /** Contact ids (for example selected elsewhere) offered as "add to the audience" in the launch tab. */
55
+ contactIds?: readonly number[];
56
+ renderRowActions?: (nurture: Nurture) => ReactNode;
57
+ renderDetailActions?: (nurture: Nurture) => ReactNode;
58
+ /** Shown in the launch tab for adding people to the audience (for example a contact picker from your CRM page). */
59
+ renderAudienceTools?: (nurture: Nurture) => ReactNode;
60
+ extraDetailTabs?: { id: string; label: string; render: (nurture: Nurture) => ReactNode }[];
61
+ toolbarActions?: ReactNode;
62
+ className?: string;
63
+ }
64
+
65
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
66
+
67
+ function toneOf(n: Nurture): Tone {
68
+ if (isLiveLike(n)) return "positive";
69
+ if (isPaused(n)) return "warning";
70
+ if (isFinished(n)) return "neutral";
71
+ return "info";
72
+ }
73
+
74
+ function matchesFilter(n: Nurture, f: string): boolean {
75
+ if (!f) return true;
76
+ if (f === "drafted") return isDrafted(n);
77
+ if (f === "live") return isLiveLike(n);
78
+ if (f === "paused") return isPaused(n);
79
+ return isFinished(n);
80
+ }
81
+
82
+ interface StepRow { step_order?: number; time_interval?: number; step_data?: { subject?: string; body?: string } }
83
+
84
+ const when = (v: string | null | undefined): string => (v ? new Date(v).toLocaleString() : "");
85
+ const yesNo = (v: boolean | undefined, fallback: boolean): string => ((v ?? fallback) ? "Yes" : "No");
86
+
87
+ /** Every field of one nurture, its audience and sender by name, and each email of the series. */
88
+ function NurtureDetails({ nurtureId }: { nurtureId: string }) {
89
+ const one = useG8Query((signal) => g8Get<Nurture>(`/nurtures/${encodeURIComponent(nurtureId)}`, {}, { signal }).then((r) => r.data), [nurtureId]);
90
+ const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
91
+ const boxes = useG8Query((signal) => g8Get<{ mailboxes?: MailboxOption[] }>("/mailboxes/my", {}, { signal }).then((r) => r.data?.mailboxes ?? []), []);
92
+ if (one.error) return <ApiError error={one.error} title="Could not load the nurture" onRetry={() => void one.refetch()} />;
93
+ if (!one.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
94
+ const n = one.data;
95
+ const list = (lists.data ?? []).find((l) => l.id === n.associated_list_id);
96
+ const box = (boxes.data ?? []).find((m) => m.id === n.pinned_mailbox_id);
97
+ const steps = Array.isArray(n.steps) ? (n.steps as StepRow[]) : [];
98
+ return (
99
+ <div className="grid gap-4">
100
+ <KeyValueList
101
+ columns={2}
102
+ items={[
103
+ { label: "Name", value: n.name },
104
+ { label: "Status", value: statusOf(n) },
105
+ { label: "Description", value: n.description || "Not set" },
106
+ { label: "Audience list", value: list ? `${list.title} (${(list.contact_count ?? 0).toLocaleString()})` : n.associated_list_id ? `List ${n.associated_list_id}` : "None yet" },
107
+ { label: "Sends from", value: box?.email ?? (n.pinned_mailbox_id ? `Mailbox ${n.pinned_mailbox_id}` : "Not set") },
108
+ { label: "Stops on reply", value: yesNo(n.finish_on_reply, true) },
109
+ { label: "One thread", value: yesNo(n.send_in_same_thread, true) },
110
+ { label: "Opt-out line", value: yesNo(n.include_unsubscribe_cta, true) },
111
+ { label: "Shared", value: yesNo(n.is_shared, false) },
112
+ { label: "Created", value: when(n.created_at) || "Not set" },
113
+ { label: "Updated", value: when(n.updated_at) || "Not set" },
114
+ { label: "Nurture id", value: n.id },
115
+ ]}
116
+ />
117
+ <SectionCard title="Emails" description={steps.length ? "In the order they are sent." : "The series has no emails to show."}>
118
+ <ol className="grid gap-2">
119
+ {steps.map((st, i) => (
120
+ <li key={st.step_order ?? i} className="rounded-md border p-3 text-sm">
121
+ <p className="font-medium">{i + 1}. {st.step_data?.subject || "No subject"}</p>
122
+ <p className="text-muted-foreground text-xs">{i === 0 ? "Sent first, when you go live" : waitText(Math.round((st.time_interval ?? 0) / 86400))}</p>
123
+ {st.step_data?.body ? <p className="mt-1 whitespace-pre-wrap">{st.step_data.body}</p> : null}
124
+ </li>
125
+ ))}
126
+ </ol>
127
+ </SectionCard>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ /** "Wait 1 day" / "Wait 3 days" after the previous email (`time_interval` is seconds). */
133
+ function waitText(days: number): string {
134
+ return `Wait ${plural(days, "day")} after the previous email`;
135
+ }
136
+
137
+ export function NurtureScreen({ connectMailboxUrl: connectMailboxUrlProp, contactIds, renderRowActions, renderDetailActions, renderAudienceTools, extraDetailTabs, toolbarActions, className }: NurtureScreenProps) {
138
+ const brand = useBrand();
139
+ const connectMailboxUrl = connectMailboxUrlProp ?? brand.appUrl;
140
+ const [id, setId] = useUrlState("id", "");
141
+ const [view, setView] = useUrlState("view", "");
142
+ const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
143
+ const [search, setSearch] = useState("");
144
+ const [filter, setFilter] = useState("");
145
+ const [creating, setCreating] = useState(false);
146
+ const [toResume, setToResume] = useState<Nurture | null>(null);
147
+ const [toArchive, setToArchive] = useState<Nurture | null>(null);
148
+ const [busy, setBusy] = useState<string | null>(null);
149
+
150
+ const list = useG8Query((signal) => g8Get<{ nurtures?: Nurture[] }>("/nurtures", { page: 1, limit: 100 }, { signal }).then((r) => r.data?.nurtures ?? []), []);
151
+ const mailboxes = useG8Query((signal) => g8Get<{ mailboxes?: MailboxOption[] }>("/mailboxes/my", {}, { signal }).then((r) => (r.data?.mailboxes ?? []).filter((m) => !m.is_archived)), [], { enabled: creating });
152
+
153
+ 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() });
154
+ const run = async (kind: "pause" | "duplicate", nurture: Nurture): Promise<void> => {
155
+ setBusy(`${kind}:${nurture.id}`);
156
+ await act.mutate({ kind, nurture });
157
+ setBusy(null);
158
+ };
159
+ const resume = useG8Mutation((n: Nurture) => g8Post(`/nurtures/${encodeURIComponent(n.id)}/resume`), { onSuccess: () => { setToResume(null); void list.refetch(); } });
160
+ const archive = useG8Mutation((n: Nurture) => g8Delete(`/nurtures/${encodeURIComponent(n.id)}`), {
161
+ onSuccess: (_r, n) => { setToArchive(null); if (n.id === id) setId(null); void list.refetch(); },
162
+ });
163
+
164
+ if (isNurtureOff(list.error)) {
165
+ 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>;
166
+ }
167
+
168
+ const all = list.data ?? [];
169
+ const q = search.trim().toLowerCase();
170
+ const shown = all.filter((n) => matchesFilter(n, filter) && (!q || n.name.toLowerCase().includes(q)));
171
+ const selected = all.find((n) => n.id === id);
172
+ const noMailbox = creating && !mailboxes.loading && (mailboxes.data ?? []).length === 0 && !mailboxes.error;
173
+
174
+ const rows = list.error ? <ApiError error={list.error} title="Could not load nurtures" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? (
175
+ <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>
176
+ ) : shown.length === 0 ? (
177
+ <EmptyState icon={<HeartHandshake className="size-8" />} title={all.length === 0 ? "No nurtures yet" : "No nurtures match"} description={all.length === 0 ? "A nurture sends a short series of emails to people who agreed to hear from you, and stops for each person when they reply." : "Try another search or status."} action={all.length === 0 ? <Button onClick={() => setCreating(true)}>New nurture</Button> : undefined} />
178
+ ) : (
179
+ <ul className="grid gap-2">
180
+ {shown.map((n) => (
181
+ <li key={n.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${n.id === id ? "bg-muted" : ""}`}>
182
+ <button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(n.id)}>
183
+ <span className="block truncate font-medium">{n.name}</span>
184
+ <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>
185
+ </button>
186
+ <StatusPill tone={toneOf(n)}>{statusOf(n)}</StatusPill>
187
+ <div className="flex gap-1">
188
+ {isLiveLike(n) ? <Button size="sm" variant="ghost" aria-label={`Pause ${n.name}`} loading={busy === `pause:${n.id}`} disabled={busy !== null} onClick={() => void run("pause", n)}><Pause className="size-4" /></Button> : null}
189
+ {isPaused(n) ? <Button size="sm" variant="ghost" aria-label={`Resume ${n.name}`} onClick={() => setToResume(n)}><Play className="size-4" /></Button> : null}
190
+ <Button size="sm" variant="ghost" aria-label={`Duplicate ${n.name}`} loading={busy === `duplicate:${n.id}`} disabled={busy !== null} onClick={() => void run("duplicate", n)}><Copy className="size-4" /></Button>
191
+ {!isFinished(n) ? <Button size="sm" variant="ghost" aria-label={`Archive ${n.name}`} onClick={() => setToArchive(n)}><Trash2 className="size-4" /></Button> : null}
192
+ {renderRowActions?.(n)}
193
+ </div>
194
+ </li>
195
+ ))}
196
+ </ul>
197
+ );
198
+
199
+ const detail = selected ? (
200
+ <DetailPanel
201
+ title={selected.name}
202
+ subtitle={selected.description ?? undefined}
203
+ badges={<StatusPill tone={toneOf(selected)}>{statusOf(selected)}</StatusPill>}
204
+ actions={renderDetailActions?.(selected)}
205
+ onClose={() => { setId(null); setView(null); }}
206
+ {...expandProps}
207
+ tabs={[
208
+ { id: "overview", label: "Overview", content: <NurtureStats key={selected.id} nurtureId={selected.id} /> },
209
+ { id: "details", label: "Details", content: <NurtureDetails key={selected.id} nurtureId={selected.id} /> },
210
+ { id: "launch", label: "Audience and launch", content: (
211
+ <div className="grid gap-4">
212
+ <NurtureEnroll key={selected.id} nurtureId={selected.id} contactIds={contactIds} onLaunched={() => void list.refetch()} />
213
+ <SectionCard title="Add people to the audience" description="Enrolling means putting a contact in the audience list. Add people to that list from your contacts, or import a list, before you go live.">
214
+ {renderAudienceTools?.(selected)}
215
+ </SectionCard>
216
+ </div>
217
+ ) },
218
+ ...(!isFinished(selected) ? [{ id: "settings", label: "Settings", content: <NurtureForm key={selected.id} nurture={selected} onSaved={() => void list.refetch()} /> }] : []),
219
+ ...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
220
+ ]}
221
+ />
222
+ ) : null;
223
+
224
+ return (
225
+ <ScreenLayout
226
+ className={className}
227
+ title="Nurture"
228
+ description="Lifecycle email series for people who agreed to hear from you."
229
+ icon={<HeartHandshake className="size-5" />}
230
+ actions={<Button size="sm" onClick={() => setCreating(true)}><Plus className="size-4" />New nurture</Button>}
231
+ stats={[
232
+ { label: "Nurtures", value: all.length.toLocaleString() },
233
+ { label: "Live", value: all.filter(isLiveLike).length.toLocaleString(), tone: "positive" },
234
+ { label: "Drafted", value: all.filter(isDrafted).length.toLocaleString() },
235
+ { label: "Paused", value: all.filter(isPaused).length.toLocaleString(), tone: "warning" },
236
+ ]}
237
+ toolbar={(
238
+ <Toolbar
239
+ search={{ value: search, onChange: setSearch, placeholder: "Search nurtures" }}
240
+ filters={(
241
+ <select aria-label="Nurture status" className={SELECT} value={filter} onChange={(e) => setFilter(e.target.value)}>
242
+ <option value="">All statuses</option><option value="drafted">Drafted</option><option value="live">Live</option><option value="paused">Paused</option><option value="finished">Finished</option>
243
+ </select>
244
+ )}
245
+ actions={toolbarActions}
246
+ />
247
+ )}
248
+ >
249
+ {act.error ? <ApiError error={act.error} title="That did not work" /> : null}
250
+ <SplitView list={rows} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => { setId(null); setView(null); }} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)} detailWidth="xl" />
251
+ <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.">
252
+ <div className="grid gap-4">
253
+ {noMailbox ? (
254
+ <SectionCard title="Connect a mailbox first" description={`A nurture sends from one connected mailbox. Google and Microsoft sign-in only works inside ${brand.linkLabel}.`}>
255
+ <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>
256
+ </SectionCard>
257
+ ) : null}
258
+ {creating ? <NurtureForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); void list.refetch(); setId(created.id); }} /> : null}
259
+ </div>
260
+ </FormSheet>
261
+ <ConfirmSpendDialog
262
+ open={toResume !== null}
263
+ tier="external"
264
+ title={`Resume ${toResume?.name ?? "this nurture"}?`}
265
+ description={<><p>Emails start going out again to the contacts who have not finished it.</p>{resume.error ? <ApiError error={resume.error} title="Could not resume" /> : null}</>}
266
+ confirmLabel="Resume"
267
+ confirming={resume.loading}
268
+ onCancel={() => setToResume(null)}
269
+ onConfirm={() => { if (toResume) void resume.mutate(toResume); }}
270
+ />
271
+ <ConfirmSpendDialog
272
+ open={toArchive !== null}
273
+ tier="destructive"
274
+ title={`Archive ${toArchive?.name ?? "this nurture"}?`}
275
+ description={<><p>{toArchive && isLiveLike(toArchive) ? "It is paused and wound down first, then archived. Contacts stop receiving its emails." : "It is archived and no longer runs."}</p>{archive.error ? <ApiError error={archive.error} title="Could not archive" /> : null}</>}
276
+ confirmLabel="Archive"
277
+ confirming={archive.loading}
278
+ onCancel={() => setToArchive(null)}
279
+ onConfirm={() => { if (toArchive) void archive.mutate(toArchive); }}
280
+ />
281
+ </ScreenLayout>
282
+ );
283
+ }
@@ -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
  ```
@@ -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. |
@@ -8,32 +8,33 @@
8
8
  "generic records table for any object",
9
9
  "record detail with owners, relationships and change history",
10
10
  "optimistic concurrency on edits",
11
- "archive and restore with history kept"
11
+ "archive and restore with history kept",
12
+ "finished ObjectsScreen composing the picker, table, record detail and schema editor"
12
13
  ],
13
14
  "dependsOn": [
14
15
  "foundation"
15
16
  ],
16
17
  "operations": [
17
- "list_objects_objects_get",
18
- "create_object_objects_post",
19
- "get_object_objects__object_slug__get",
20
- "list_object_attributes_objects__object_slug__attributes_get",
21
- "update_object_objects__object_slug__patch",
22
- "create_attribute_objects__object_slug__attributes_post",
23
- "update_attribute_objects__object_slug__attributes__attr_slug__patch",
24
18
  "archive_attribute_objects__object_slug__attributes__attr_slug__delete",
25
19
  "archive_object_objects__object_slug__delete",
26
- "list_object_records_objects__object_slug__records_get",
27
- "create_object_record_objects__object_slug__records_post",
28
- "update_object_record_objects__object_slug__records__record_id__patch",
29
20
  "archive_object_record_objects__object_slug__records__record_id__delete",
30
- "get_object_record_objects__object_slug__records__record_id__get",
21
+ "assign_record_role_records__entity_type___entity_id__role_assignments_post",
22
+ "clear_record_role_records__entity_type___entity_id__role_assignments__role_slot__delete",
23
+ "create_attribute_objects__object_slug__attributes_post",
24
+ "create_object_objects_post",
25
+ "create_object_record_objects__object_slug__records_post",
26
+ "get_object_objects__object_slug__get",
31
27
  "get_object_record_changes_objects__object_slug__records__record_id__changes_get",
28
+ "get_object_record_objects__object_slug__records__record_id__get",
32
29
  "get_object_relationships_objects__object_slug__records__record_id__relationships_get",
33
- "restore_object_record_objects__object_slug__records__record_id__restore_post",
34
30
  "get_record_role_slots_records__entity_type___entity_id__role_slots_get",
35
- "assign_record_role_records__entity_type___entity_id__role_assignments_post",
36
- "clear_record_role_records__entity_type___entity_id__role_assignments__role_slot__delete"
31
+ "list_object_attributes_objects__object_slug__attributes_get",
32
+ "list_object_records_objects__object_slug__records_get",
33
+ "list_objects_objects_get",
34
+ "restore_object_record_objects__object_slug__records__record_id__restore_post",
35
+ "update_attribute_objects__object_slug__attributes__attr_slug__patch",
36
+ "update_object_objects__object_slug__patch",
37
+ "update_object_record_objects__object_slug__records__record_id__patch"
37
38
  ],
38
39
  "components": [
39
40
  {
@@ -151,6 +152,17 @@
151
152
  "write",
152
153
  "destructive"
153
154
  ]
155
+ },
156
+ {
157
+ "name": "ObjectsScreen",
158
+ "file": "components/objects-screen.tsx",
159
+ "purpose": "Finished custom-objects workspace: object picker, records table, record detail panel and the schema editor in a sheet. No required props.",
160
+ "operations": [
161
+ "list_objects_objects_get"
162
+ ],
163
+ "tiers": [
164
+ "read"
165
+ ]
154
166
  }
155
167
  ],
156
168
  "hooks": [
@@ -238,6 +250,7 @@
238
250
  }
239
251
  ],
240
252
  "peerDependencies": {
241
- "react": ">=18"
253
+ "react": ">=18",
254
+ "lucide-react": ">=0.400.0"
242
255
  }
243
256
  }
@@ -15,6 +15,7 @@ import { EmptyState } from "@/g8/components/empty-state";
15
15
  import { g8Get } from "@/g8/lib/g8-client";
16
16
  import { useG8Query } from "@/g8/hooks/use-g8-query";
17
17
  import { isObjectsApiMissing } from "../lib/object-types";
18
+ import { useBrand } from "@/g8/hooks/use-brand";
18
19
 
19
20
  export interface ObjectNotFoundProps {
20
21
  /** What was not found, for the message: "record" or "object". */
@@ -24,10 +25,11 @@ export interface ObjectNotFoundProps {
24
25
  }
25
26
 
26
27
  export function ObjectNotFound({ what, error }: ObjectNotFoundProps) {
28
+ const brand = useBrand();
27
29
  const probe = useG8Query((signal) => g8Get<unknown[]>("/objects", {}, { signal }).then(() => "enabled" as const), []);
28
30
  if (probe.loading && !probe.data && !probe.error) return <p className="text-muted-foreground text-sm" aria-busy="true">Checking...</p>;
29
31
  if (isObjectsApiMissing(probe.error)) {
30
- return <EmptyState title="Custom objects are unavailable" description="The graph8 API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current graph8 backend, then reload." />;
32
+ return <EmptyState title="Custom objects are unavailable" description={`The ${brand.linkLabel} API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current ${brand.linkLabel} backend, then reload.`} />;
31
33
  }
32
34
  if (probe.error) return <ApiError error={probe.error} title="Could not check custom objects" onRetry={() => void probe.refetch()} />;
33
35
  return <EmptyState title={`This ${what} no longer exists`} description={`It may have been deleted, or the link is out of date. Go back and pick another ${what}.`} />;
@@ -59,7 +59,8 @@ export function ObjectRecordDetail({ objectSlug, recordId, onArchived, onBack }:
59
59
 
60
60
  if (isObjectsApiMissing(record.error)) return <ObjectNotFound what="record" error={record.error} />;
61
61
  if (record.error) return <ApiError error={record.error} title="Could not load the record" onRetry={() => void record.refetch()} />;
62
- if (!record.data || schema.loading) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-64" /><Skeleton className="h-40 w-full" /></div>;
62
+ // The hook keeps the previous record while the next loads; edit and archive act on recordId, so show only that record.
63
+ if (!record.data || String(record.data.id) !== String(recordId) || schema.loading) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-64" /><Skeleton className="h-40 w-full" /></div>;
63
64
 
64
65
  const current = record.data;
65
66
  const roleEntity = ROLE_ENTITY_TYPES[objectSlug];