@graph8/sdk 0.362.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 (349) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +64 -1
  3. package/blocks/_foundation/block.json +79 -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/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/screen/index.ts +1 -0
  9. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  10. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  11. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  12. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  13. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  14. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  15. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  16. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  17. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  18. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  19. package/blocks/_foundation/lib/g8-client.ts +1 -1
  20. package/blocks/_foundation/lib/plural.ts +23 -0
  21. package/blocks/_foundation/server/connect-route.ts +15 -0
  22. package/blocks/_foundation/server/connect.ts +127 -0
  23. package/blocks/_foundation/server/local-key.ts +70 -0
  24. package/blocks/_foundation/server/proxy.ts +103 -3
  25. package/blocks/_foundation/server/route.ts +16 -4
  26. package/blocks/agents/README.md +14 -0
  27. package/blocks/agents/block.json +33 -1
  28. package/blocks/agents/components/agent-builder.tsx +13 -8
  29. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  30. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  31. package/blocks/agents/components/agent-runs.tsx +16 -7
  32. package/blocks/agents/components/agents-screen.tsx +184 -0
  33. package/blocks/analytics/README.md +13 -0
  34. package/blocks/analytics/block.json +45 -1
  35. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  36. package/blocks/analytics/components/leaderboard.tsx +14 -7
  37. package/blocks/analytics/components/report-builder.tsx +23 -9
  38. package/blocks/analytics/components/report-detail.tsx +84 -0
  39. package/blocks/analytics/components/trend-chart.tsx +62 -0
  40. package/blocks/appointments/block.json +6 -2
  41. package/blocks/appointments/components/availability-editor.tsx +17 -5
  42. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  43. package/blocks/appointments/components/event-type-related.tsx +4 -2
  44. package/blocks/appointments/components/meeting-list.tsx +9 -5
  45. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  46. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  47. package/blocks/block.schema.json +18 -0
  48. package/blocks/block.types.ts +17 -0
  49. package/blocks/campaigns/README.md +41 -11
  50. package/blocks/campaigns/block.json +105 -42
  51. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  52. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  53. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  54. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  55. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  56. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  57. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  58. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  59. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  60. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  61. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  62. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  63. package/blocks/coach/README.md +10 -0
  64. package/blocks/coach/block.json +16 -1
  65. package/blocks/coach/components/coach-screen.tsx +222 -0
  66. package/blocks/copilot/README.md +13 -0
  67. package/blocks/copilot/block.json +14 -1
  68. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  69. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  70. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  71. package/blocks/crm/README.md +28 -0
  72. package/blocks/crm/block.json +58 -2
  73. package/blocks/crm/components/companies-screen.tsx +345 -0
  74. package/blocks/crm/components/company-detail.tsx +8 -6
  75. package/blocks/crm/components/company-table.tsx +11 -4
  76. package/blocks/crm/components/contact-detail.tsx +6 -3
  77. package/blocks/crm/components/contact-table.tsx +10 -3
  78. package/blocks/crm/components/contacts-screen.tsx +359 -0
  79. package/blocks/crm/components/dedupe-review.tsx +7 -5
  80. package/blocks/crm/components/import-wizard.tsx +4 -2
  81. package/blocks/crm/components/notes-panel.tsx +4 -4
  82. package/blocks/crm/components/record-form.tsx +5 -3
  83. package/blocks/crm/components/task-list.tsx +4 -4
  84. package/blocks/crm/components/tasks-screen.tsx +379 -0
  85. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  86. package/blocks/crm/hooks/use-page-query.ts +7 -1
  87. package/blocks/crm/lib/crm-types.ts +12 -2
  88. package/blocks/crm/lib/screen-parts.tsx +105 -0
  89. package/blocks/deals/block.json +9 -3
  90. package/blocks/deals/components/deal-detail.tsx +23 -11
  91. package/blocks/deals/components/deal-related.tsx +1 -1
  92. package/blocks/deals/components/deals-screen.tsx +10 -5
  93. package/blocks/deals/components/stage-editor.tsx +4 -4
  94. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  95. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  96. package/blocks/dialer/README.md +34 -2
  97. package/blocks/dialer/block.json +67 -1
  98. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  99. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  100. package/blocks/dialer/components/call-log.tsx +7 -3
  101. package/blocks/dialer/components/call-queue.tsx +1 -1
  102. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  103. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  104. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  105. package/blocks/dialer/components/session-start-form.tsx +4 -2
  106. package/blocks/dialer/components/softphone.tsx +3 -1
  107. package/blocks/enrichment/README.md +14 -2
  108. package/blocks/enrichment/block.json +32 -4
  109. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  110. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  111. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  112. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  113. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  114. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  115. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  116. package/blocks/forms/block.json +32 -18
  117. package/blocks/forms/components/form-builder.tsx +3 -1
  118. package/blocks/forms/components/form-embed.tsx +3 -1
  119. package/blocks/forms/components/forms-screen.tsx +5 -2
  120. package/blocks/forms/components/submissions-table.tsx +3 -1
  121. package/blocks/inbox/README.md +27 -3
  122. package/blocks/inbox/block.json +14 -2
  123. package/blocks/inbox/components/inbox-list.tsx +7 -3
  124. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  125. package/blocks/inbox/components/reply-composer.tsx +65 -9
  126. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  127. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  128. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  129. package/blocks/inbox/lib/inbox-types.ts +47 -1
  130. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  131. package/blocks/integrations/README.md +9 -0
  132. package/blocks/integrations/block.json +46 -1
  133. package/blocks/integrations/components/crm-detail.tsx +100 -0
  134. package/blocks/integrations/components/integration-list.tsx +9 -4
  135. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  136. package/blocks/integrations/components/sync-status.tsx +5 -3
  137. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  138. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  139. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  140. package/blocks/intent/README.md +14 -0
  141. package/blocks/intent/block.json +27 -1
  142. package/blocks/intent/components/intent-feed.tsx +5 -2
  143. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  144. package/blocks/intent/components/intent-screen.tsx +265 -0
  145. package/blocks/intent/components/keyword-detail.tsx +124 -0
  146. package/blocks/lifecycle/block.json +13 -8
  147. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  148. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  149. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  150. package/blocks/lists/README.md +13 -0
  151. package/blocks/lists/block.json +39 -1
  152. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  153. package/blocks/lists/components/list-picker.tsx +5 -1
  154. package/blocks/lists/components/list-table.tsx +11 -5
  155. package/blocks/lists/components/lists-screen.tsx +344 -0
  156. package/blocks/lists/components/suppression-manager.tsx +6 -2
  157. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  158. package/blocks/mailboxes/README.md +17 -0
  159. package/blocks/mailboxes/block.json +65 -17
  160. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  161. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  162. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  163. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  164. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  165. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  166. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  167. package/blocks/newsletter/block.json +40 -24
  168. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  169. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  170. package/blocks/nurture/block.json +28 -14
  171. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  172. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  173. package/blocks/nurture/components/nurture-form.tsx +23 -3
  174. package/blocks/nurture/components/nurture-screen.tsx +10 -6
  175. package/blocks/nurture/lib/nurture-types.ts +1 -1
  176. package/blocks/objects/README.md +17 -0
  177. package/blocks/objects/block.json +29 -16
  178. package/blocks/objects/components/object-not-found.tsx +3 -1
  179. package/blocks/objects/components/object-record-detail.tsx +2 -1
  180. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  181. package/blocks/objects/components/object-table.tsx +13 -3
  182. package/blocks/objects/components/objects-screen.tsx +147 -0
  183. package/blocks/objects/lib/object-types.ts +1 -1
  184. package/blocks/pages/block.json +28 -23
  185. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  186. package/blocks/pages/components/content-library.tsx +9 -4
  187. package/blocks/pages/components/page-list.tsx +2 -1
  188. package/blocks/quotes/block.json +17 -12
  189. package/blocks/quotes/components/product-picker.tsx +3 -1
  190. package/blocks/quotes/components/quote-related.tsx +5 -2
  191. package/blocks/radar/README.md +10 -0
  192. package/blocks/radar/block.json +41 -1
  193. package/blocks/radar/components/competitor-detail.tsx +117 -0
  194. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  195. package/blocks/radar/components/radar-report.tsx +4 -2
  196. package/blocks/radar/components/radar-screen.tsx +214 -0
  197. package/blocks/radar/components/report-detail.tsx +104 -0
  198. package/blocks/search/README.md +10 -0
  199. package/blocks/search/block.json +16 -1
  200. package/blocks/search/components/filter-builder.tsx +3 -3
  201. package/blocks/search/components/prospect-results.tsx +5 -3
  202. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  203. package/blocks/search/components/saved-searches.tsx +4 -1
  204. package/blocks/search/hooks/use-filter-options.ts +20 -6
  205. package/blocks/search/lib/search-types.ts +2 -0
  206. package/blocks/sequencer/README.md +49 -9
  207. package/blocks/sequencer/block.json +99 -19
  208. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  209. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  210. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  211. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  212. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  213. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  214. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  215. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  216. package/blocks/sequencer/components/step-editor.tsx +3 -1
  217. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  218. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  219. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  220. package/blocks/social-ads/README.md +11 -0
  221. package/blocks/social-ads/block.json +101 -2
  222. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  223. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  224. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  225. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  226. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  227. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  228. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  229. package/blocks/social-ads/components/post-composer.tsx +3 -1
  230. package/blocks/social-ads/components/post-detail.tsx +160 -0
  231. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  232. package/blocks/social-ads/components/social-screen.tsx +160 -0
  233. package/blocks/visitors/README.md +10 -0
  234. package/blocks/visitors/block.json +25 -1
  235. package/blocks/visitors/components/snippet-install.tsx +3 -1
  236. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  237. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  238. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  239. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  240. package/blocks/webchat/README.md +10 -0
  241. package/blocks/webchat/block.json +21 -7
  242. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  243. package/blocks/webchat/components/conversation-list.tsx +7 -3
  244. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  245. package/blocks/workbench/README.md +11 -0
  246. package/blocks/workbench/block.json +48 -1
  247. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  248. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  249. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  250. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  251. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  252. package/blocks/workflows/README.md +13 -0
  253. package/blocks/workflows/block.json +28 -9
  254. package/blocks/workflows/components/run-history.tsx +31 -9
  255. package/blocks/workflows/components/trigger-config.tsx +3 -1
  256. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  257. package/blocks/workflows/components/workflow-list.tsx +13 -6
  258. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  259. package/dist/cli.js +2709 -1721
  260. package/dist/errors.d.mts +34 -0
  261. package/dist/errors.d.ts +34 -0
  262. package/dist/errors.js +46 -0
  263. package/dist/errors.js.map +1 -0
  264. package/dist/errors.mjs +21 -0
  265. package/dist/errors.mjs.map +1 -0
  266. package/dist/index.browser.js +11 -5
  267. package/dist/index.browser.js.map +1 -1
  268. package/dist/index.browser.mjs +11 -5
  269. package/dist/index.browser.mjs.map +1 -1
  270. package/dist/index.d.mts +164 -25
  271. package/dist/index.d.ts +164 -25
  272. package/dist/index.js +11 -5
  273. package/dist/index.js.map +1 -1
  274. package/dist/index.mjs +11 -5
  275. package/dist/index.mjs.map +1 -1
  276. package/dist/react.browser.js +10 -4
  277. package/dist/react.browser.js.map +1 -1
  278. package/dist/react.browser.mjs +10 -4
  279. package/dist/react.browser.mjs.map +1 -1
  280. package/dist/react.d.mts +142 -5
  281. package/dist/react.d.ts +142 -5
  282. package/dist/react.js +10 -4
  283. package/dist/react.js.map +1 -1
  284. package/dist/react.mjs +10 -4
  285. package/dist/react.mjs.map +1 -1
  286. package/dist/types/blocks/add.d.ts +11 -4
  287. package/dist/types/blocks/brand.d.ts +113 -0
  288. package/dist/types/blocks/compose.d.ts +53 -0
  289. package/dist/types/blocks/create.d.ts +22 -19
  290. package/dist/types/blocks/doctor.d.ts +5 -1
  291. package/dist/types/blocks/navfile.d.ts +35 -0
  292. package/dist/types/blocks/paths.d.ts +4 -0
  293. package/dist/types/blocks/prefixes.d.ts +37 -0
  294. package/dist/types/blocks/registry.d.ts +17 -0
  295. package/dist/types/blocks/screens.d.ts +53 -0
  296. package/dist/types/cli.d.ts +2 -0
  297. package/dist/types/errors.d.ts +33 -0
  298. package/dist/types/http.d.ts +2 -18
  299. package/dist/types/version.d.ts +2 -2
  300. package/docs/agents.md +15 -4
  301. package/docs/api/README.md +2 -2
  302. package/docs/api/by-path.md +1 -0
  303. package/docs/api/desk.d.ts +98 -2
  304. package/docs/api/desk.md +123 -1
  305. package/docs/api/inbox.d.ts +10 -2
  306. package/docs/api/inbox.md +10 -2
  307. package/docs/api/seo.d.ts +19 -1
  308. package/docs/api/seo.md +24 -1
  309. package/package.json +16 -2
  310. package/stacks/6sense.json +41 -16
  311. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  312. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  313. package/stacks/apollo.json +118 -36
  314. package/stacks/calendly.json +60 -0
  315. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  316. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  317. package/stacks/clay.json +59 -26
  318. package/stacks/gong.json +81 -35
  319. package/stacks/hootsuite.json +99 -0
  320. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  321. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  322. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  323. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  324. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  325. package/stacks/hubspot.json +167 -66
  326. package/stacks/intercom.json +48 -20
  327. package/stacks/mailchimp.json +52 -33
  328. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  329. package/stacks/outreach.json +88 -25
  330. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  331. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  332. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  333. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  334. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  335. package/stacks/pipedrive.json +115 -0
  336. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  337. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  338. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  339. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  340. package/stacks/salesforce.json +81 -31
  341. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  342. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  343. package/stacks/zoominfo.json +102 -0
  344. package/styles/styles.css +1 -1
  345. package/templates/next-app/README.md.tmpl +40 -9
  346. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  347. package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
  348. package/templates/next-app/dot-gitignore.tmpl +2 -0
  349. package/templates/next-app/package.json.tmpl +1 -1
@@ -15,40 +15,55 @@
15
15
  "lists"
16
16
  ],
17
17
  "operations": [
18
- "list_newsletters_newsletters_get",
19
- "create_newsletter_newsletters_post",
20
- "list_issues_newsletters__newsletter_id__issues_get",
21
18
  "activate_newsletter_newsletters__newsletter_id__activate_post",
22
- "pause_newsletter_newsletters__newsletter_id__pause_post",
23
- "get_issue_newsletters__newsletter_id__issues__issue_id__get",
19
+ "add_subscriber_newsletters__newsletter_id__subscribers_post",
20
+ "cancel_send_newsletters__newsletter_id__issues__issue_id__send_cancel_post",
24
21
  "create_issue_newsletters__newsletter_id__issues_post",
25
- "update_issue_newsletters__newsletter_id__issues__issue_id__put",
22
+ "create_newsletter_newsletters_post",
23
+ "delete_issue_newsletters__newsletter_id__issues__issue_id__delete",
24
+ "delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
26
25
  "generate_newsletter_issue_newsletters__newsletter_id__issues__issue_id__generate_post",
26
+ "get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get",
27
27
  "get_issue_generation_status_newsletters__newsletter_id__issues__issue_id__generation_status_get",
28
- "send_test_email_newsletters__newsletter_id__issues__issue_id__send_test_post",
29
- "delete_issue_newsletters__newsletter_id__issues__issue_id__delete",
30
- "list_subscribers_newsletters__newsletter_id__subscribers_get",
31
- "send_issue_newsletters__newsletter_id__issues__issue_id__send_post",
28
+ "get_issue_newsletters__newsletter_id__issues__issue_id__get",
32
29
  "get_send_status_newsletters__newsletter_id__issues__issue_id__send_get",
33
- "pause_send_newsletters__newsletter_id__issues__issue_id__send_pause_post",
34
- "resume_send_newsletters__newsletter_id__issues__issue_id__send_resume_post",
35
- "cancel_send_newsletters__newsletter_id__issues__issue_id__send_cancel_post",
36
- "add_subscriber_newsletters__newsletter_id__subscribers_post",
37
- "list_lists_lists_get",
38
30
  "import_subscribers_from_audience_newsletters__newsletter_id__subscribers_import_audience_post",
31
+ "list_issues_newsletters__newsletter_id__issues_get",
32
+ "list_lists_lists_get",
33
+ "list_newsletters_newsletters_get",
34
+ "list_subscribers_newsletters__newsletter_id__subscribers_get",
35
+ "pause_newsletter_newsletters__newsletter_id__pause_post",
36
+ "pause_send_newsletters__newsletter_id__issues__issue_id__send_pause_post",
39
37
  "preview_newsletter_subscriber_verification_newsletters__newsletter_id__subscribers_verify_preview_post",
40
- "verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post",
38
+ "resume_send_newsletters__newsletter_id__issues__issue_id__send_resume_post",
39
+ "send_issue_newsletters__newsletter_id__issues__issue_id__send_post",
40
+ "send_test_email_newsletters__newsletter_id__issues__issue_id__send_test_post",
41
41
  "unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch",
42
- "delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
43
- "get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get"
42
+ "update_issue_newsletters__newsletter_id__issues__issue_id__put",
43
+ "verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post"
44
44
  ],
45
45
  "components": [
46
46
  {
47
47
  "name": "NewsletterScreen",
48
48
  "file": "components/newsletter-screen.tsx",
49
49
  "purpose": "Finished newsletter workspace: pick or create a newsletter, issues list with detail (write, send, results), subscribers and performance tabs.",
50
- "operations": [],
51
- "tiers": []
50
+ "operations": [
51
+ "activate_newsletter_newsletters__newsletter_id__activate_post",
52
+ "create_newsletter_newsletters_post",
53
+ "delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
54
+ "get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get",
55
+ "list_issues_newsletters__newsletter_id__issues_get",
56
+ "list_newsletters_newsletters_get",
57
+ "list_subscribers_newsletters__newsletter_id__subscribers_get",
58
+ "pause_newsletter_newsletters__newsletter_id__pause_post",
59
+ "unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch"
60
+ ],
61
+ "tiers": [
62
+ "read",
63
+ "write",
64
+ "external",
65
+ "destructive"
66
+ ]
52
67
  },
53
68
  {
54
69
  "name": "NewsletterEditor",
@@ -112,14 +127,15 @@
112
127
  "file": "components/subscriber-table.tsx",
113
128
  "purpose": "Subscribers with search, status filter, add, import a list, verify addresses, unsubscribe and erase.",
114
129
  "operations": [
115
- "list_subscribers_newsletters__newsletter_id__subscribers_get",
116
130
  "add_subscriber_newsletters__newsletter_id__subscribers_post",
117
- "list_lists_lists_get",
131
+ "delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
118
132
  "import_subscribers_from_audience_newsletters__newsletter_id__subscribers_import_audience_post",
133
+ "list_lists_lists_get",
134
+ "list_newsletters_newsletters_get",
135
+ "list_subscribers_newsletters__newsletter_id__subscribers_get",
119
136
  "preview_newsletter_subscriber_verification_newsletters__newsletter_id__subscribers_verify_preview_post",
120
- "verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post",
121
137
  "unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch",
122
- "delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete"
138
+ "verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post"
123
139
  ],
124
140
  "tiers": [
125
141
  "read",
@@ -25,6 +25,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
25
  import { useJobPoll } from "@/g8/hooks/use-job-poll";
26
26
  import { generationInProgress, isDraft, issueTitle, sendIsMoving, sendIsPaused, toIsoInstant } from "../lib/newsletter-types";
27
27
  import type { Issue, SendState } from "../lib/newsletter-types";
28
+ import { pluralWord } from "@/g8/lib/plural";
28
29
 
29
30
  export interface IssueSchedulerProps {
30
31
  newsletterId: string;
@@ -111,7 +112,7 @@ export function IssueScheduler({ newsletterId, issueId, onSent }: IssueScheduler
111
112
  {partial ? <p role="alert" className="text-sm text-amber-700">This issue is still being written by AI. Wait for it to finish: sending now would mail the partial version.</p> : null}
112
113
  {draft ? (
113
114
  <div className="grid gap-3">
114
- <p className="text-sm">Audience: <strong>{audience.loading ? "..." : subscribers.toLocaleString()}</strong> subscribed {subscribers === 1 ? "person" : "people"}.</p>
115
+ <p className="text-sm">Audience: <strong>{audience.loading ? "..." : subscribers.toLocaleString()}</strong> subscribed {pluralWord(subscribers, "person", "people")}.</p>
115
116
  <div className="flex gap-2" role="group" aria-label="When to send">
116
117
  <Button size="sm" variant={when === "now" ? "secondary" : "ghost"} onClick={() => setWhen("now")}>Send now</Button>
117
118
  <Button size="sm" variant={when === "later" ? "secondary" : "ghost"} onClick={() => setWhen("later")}>Schedule</Button>
@@ -46,6 +46,7 @@ import { IssueForm } from "./issue-form";
46
46
  import { IssueScheduler } from "./issue-scheduler";
47
47
  import { NewsletterStats } from "./newsletter-stats";
48
48
  import { SubscriberTable } from "./subscriber-table";
49
+ import { useBrand } from "@/g8/hooks/use-brand";
49
50
 
50
51
  export interface NewsletterScreenProps {
51
52
  /** Open this newsletter first when the URL does not name one. */
@@ -170,6 +171,7 @@ function SubscriberDetail({ newsletterId, subscriber, onChanged, onClose, expand
170
171
  }
171
172
 
172
173
  export function NewsletterScreen({ initialNewsletterId, renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: NewsletterScreenProps) {
174
+ const brand = useBrand();
173
175
  const [urlNewsletter, setUrlNewsletter] = useUrlState("newsletter", initialNewsletterId ?? "");
174
176
  const [issueParam, setIssueParam] = useUrlState("id", "");
175
177
  const [view, setView] = useUrlState("view", "");
@@ -391,7 +393,7 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
391
393
  open={pending !== null}
392
394
  tier="external"
393
395
  title={pending === "activate" ? `Activate ${current?.name ?? "this newsletter"}?` : `Create the newsletter "${name.trim()}"?`}
394
- description={<><p>{pending === "activate" ? "Once active, the scheduler sends any issue whose scheduled time has passed to real subscribers. A verified sending domain is required (set it up in graph8)." : "This creates the newsletter container. Nothing is sent until you activate it and send an issue."}</p>{(create.error ?? activate.error) ? <ApiError error={create.error ?? activate.error} title="That did not work" /> : null}</>}
396
+ description={<><p>{pending === "activate" ? `Once active, the scheduler sends any issue whose scheduled time has passed to real subscribers. A verified sending domain is required (set it up in ${brand.linkLabel}).` : "This creates the newsletter container. Nothing is sent until you activate it and send an issue."}</p>{(create.error ?? activate.error) ? <ApiError error={create.error ?? activate.error} title="That did not work" /> : null}</>}
395
397
  confirmLabel={pending === "activate" ? "Activate" : "Create"}
396
398
  confirming={create.loading || activate.loading}
397
399
  onCancel={() => setPending(null)}
@@ -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": [
@@ -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>
@@ -45,6 +45,8 @@ import type { ListOption, MailboxOption, Nurture } from "../lib/nurture-types";
45
45
  import { NurtureEnroll } from "./nurture-enroll";
46
46
  import { NurtureForm } from "./nurture-form";
47
47
  import { NurtureStats } from "./nurture-stats";
48
+ import { plural } from "@/g8/lib/plural";
49
+ import { useBrand } from "@/g8/hooks/use-brand";
48
50
 
49
51
  export interface NurtureScreenProps {
50
52
  /** Where a person connects a mailbox (Google and Microsoft sign-in only works in the graph8 app). */
@@ -129,10 +131,12 @@ function NurtureDetails({ nurtureId }: { nurtureId: string }) {
129
131
 
130
132
  /** "Wait 1 day" / "Wait 3 days" after the previous email (`time_interval` is seconds). */
131
133
  function waitText(days: number): string {
132
- return `Wait ${days} ${days === 1 ? "day" : "days"} after the previous email`;
134
+ return `Wait ${plural(days, "day")} after the previous email`;
133
135
  }
134
136
 
135
- export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", contactIds, renderRowActions, renderDetailActions, renderAudienceTools, extraDetailTabs, toolbarActions, className }: NurtureScreenProps) {
137
+ export function NurtureScreen({ connectMailboxUrl: connectMailboxUrlProp, contactIds, renderRowActions, renderDetailActions, renderAudienceTools, extraDetailTabs, toolbarActions, className }: NurtureScreenProps) {
138
+ const brand = useBrand();
139
+ const connectMailboxUrl = connectMailboxUrlProp ?? brand.appUrl;
136
140
  const [id, setId] = useUrlState("id", "");
137
141
  const [view, setView] = useUrlState("view", "");
138
142
  const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
@@ -158,7 +162,7 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
158
162
  });
159
163
 
160
164
  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>;
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>;
162
166
  }
163
167
 
164
168
  const all = list.data ?? [];
@@ -177,7 +181,7 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
177
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" : ""}`}>
178
182
  <button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(n.id)}>
179
183
  <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>
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>
181
185
  </button>
182
186
  <StatusPill tone={toneOf(n)}>{statusOf(n)}</StatusPill>
183
187
  <div className="flex gap-1">
@@ -247,8 +251,8 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
247
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.">
248
252
  <div className="grid gap-4">
249
253
  {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>
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>
252
256
  </SectionCard>
253
257
  ) : null}
254
258
  {creating ? <NurtureForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); void list.refetch(); setId(created.id); }} /> : null}
@@ -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];
@@ -27,6 +27,7 @@ import { useObjectSchema } from "../hooks/use-object-schema";
27
27
  import { OBJECT_ICONS, isObjectsApiMissing, isValidSlug, slugify } from "../lib/object-types";
28
28
  import type { ObjectAttribute, ObjectDefinition } from "../lib/object-types";
29
29
  import { AttributeFormDialog } from "./attribute-form-dialog";
30
+ import { useBrand } from "@/g8/hooks/use-brand";
30
31
 
31
32
  export interface ObjectSchemaEditorProps {
32
33
  /** Object to open first. */
@@ -93,6 +94,7 @@ function ObjectDetailsForm({ object, attributes, onSaved }: { object: ObjectDefi
93
94
  }
94
95
 
95
96
  export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: ObjectSchemaEditorProps) {
97
+ const brand = useBrand();
96
98
  const [selected, setSelected] = useState<string | undefined>(initialObjectSlug);
97
99
  const [editing, setEditing] = useState<ObjectAttribute | "new" | null>(null);
98
100
  const [pending, setPending] = useState<Pending | null>(null);
@@ -113,7 +115,7 @@ export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: Object
113
115
 
114
116
  if (objects.loading && !objects.data) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-48" /><Skeleton className="h-40 w-full" /></div>;
115
117
  if (isObjectsApiMissing(objects.error)) {
116
- 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." />;
118
+ 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.`} />;
117
119
  }
118
120
  if (objects.error) return <ApiError error={objects.error} title="Could not load objects" onRetry={() => void objects.refetch()} />;
119
121
 
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { useEffect, useState } from "react";
16
+ import type { ReactNode } from "react";
16
17
  import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
17
18
  import { ApiError } from "@/g8/components/api-error";
18
19
  import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
@@ -20,6 +21,8 @@ import { EmptyState } from "@/g8/components/empty-state";
20
21
  import { g8Delete, g8Get } from "@/g8/lib/g8-client";
21
22
  import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
22
23
  import { usePaginated } from "@/g8/hooks/use-paginated";
24
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
25
+ import { RecordLink } from "@/g8/components/screen/record-link";
23
26
  import { useObjectSchema } from "../hooks/use-object-schema";
24
27
  import { formatValue, isObjectsApiMissing, recordTitle } from "../lib/object-types";
25
28
  import { ObjectNotFound } from "./object-not-found";
@@ -27,6 +30,8 @@ import type { ObjectRecord } from "../lib/object-types";
27
30
  import { RecordFormDialog } from "./record-form-dialog";
28
31
 
29
32
  export interface ObjectTableProps {
33
+ /** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
34
+ linkParam?: string;
30
35
  objectSlug: string;
31
36
  /** Rows per request, 1-200. Default 50. */
32
37
  pageSize?: number;
@@ -36,7 +41,7 @@ export interface ObjectTableProps {
36
41
  maxColumns?: number;
37
42
  }
38
43
 
39
- export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
44
+ export function ObjectTable({ linkParam, objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
40
45
  const schema = useObjectSchema(objectSlug);
41
46
  const slug = encodeURIComponent(objectSlug);
42
47
  const records = usePaginated<ObjectRecord>(
@@ -68,6 +73,11 @@ export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColu
68
73
  return <ApiError error={schema.error ?? records.error} title="Could not load records" onRetry={() => { void schema.refetch(); void records.reload(); }} />;
69
74
  }
70
75
 
76
+ const cellText = (c: (typeof columns)[number], i: number, record: ObjectRecord): ReactNode => {
77
+ const text = formatValue(c, record.values?.[c.slug]) || (i === 0 ? recordTitle(schema.definition, schema.attributes, record) : "");
78
+ return i === 0 && onSelectRecord && text ? <RecordLink param={linkParam} id={record.id} onOpen={() => onSelectRecord(record)}>{text}</RecordLink> : text;
79
+ };
80
+
71
81
  return (
72
82
  <div className="grid gap-3">
73
83
  <div className="flex items-center justify-between gap-2">
@@ -92,9 +102,9 @@ export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColu
92
102
  </TableHeader>
93
103
  <TableBody>
94
104
  {rows.map((record) => (
95
- <TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined} onClick={() => onSelectRecord?.(record)}>
105
+ <TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined} {...(onSelectRecord ? rowActivationProps(() => onSelectRecord(record)) : {})}>
96
106
  {columns.map((c, i) => (
97
- <TableCell key={c.slug}>{formatValue(c, record.values?.[c.slug]) || (i === 0 ? recordTitle(schema.definition, schema.attributes, record) : "")}</TableCell>
107
+ <TableCell key={c.slug}>{cellText(c, i, record)}</TableCell>
98
108
  ))}
99
109
  <TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
100
110
  <Button variant="ghost" size="sm" onClick={() => setForm({ record })}>Edit</Button>