@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
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ReportDetail: everything about one saved custom-object report: its definition (object, measure, grouping, filters,
5
+ * chart type, visibility, revision, owner dates), the latest result table, and the actions Run again and Delete.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. GET /reports/{id} (reports.getObjectReportApi) read the definition
9
+ * 2. POST /reports/{id}/execute (reports.executeObjectReportApi) write run it (creates a run record, sends nothing); runs once on open
10
+ * 3. DELETE /reports/{id} (reports.archiveObjectReportApi) DESTRUCTIVE -> ConfirmSpendDialog
11
+ * Editing happens in ReportBuilder (`reportId`). Preview and execute share the org's report-run slots (429 `too_many_report_runs`).
12
+ */
13
+ import { useEffect, useState } from "react";
14
+ import { Button, Skeleton } from "@graph8/sdk/ui";
15
+ import { ApiError } from "@/g8/components/api-error";
16
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
17
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
18
+ import { StatusPill } from "@/g8/components/screen/status-pill";
19
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
21
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
22
+ import { reportBody, type ReportResultData, type SavedReport } from "../lib/analytics-types";
23
+ import { ResultTable } from "./report-builder";
24
+
25
+ export interface ReportDetailProps {
26
+ reportId: string;
27
+ /** Called after the report was deleted. */
28
+ onDeleted?: () => void;
29
+ /** A result the host already holds (kept across tab switches). While set, opening does not run the report again. */
30
+ result?: ReportResultData | null;
31
+ /** Called with every fresh result, so the host can keep it. */
32
+ onResult?: (result: ReportResultData | null) => void;
33
+ className?: string;
34
+ }
35
+
36
+ type Cfg = { measures?: Array<Record<string, unknown>>; group_by?: Array<Record<string, unknown>>; filters?: unknown[] };
37
+
38
+ const measureText = (m: Record<string, unknown> | undefined): string => (m ? `${String(m.kind)}${m.attribute_slug ? ` of ${String(m.attribute_slug)}` : ""}` : "");
39
+ const groupText = (g: Record<string, unknown> | undefined): string => (g ? (g.kind === "created_at" ? `created date by ${String(g.grain ?? "month")}` : String(g.attribute_slug ?? "")) : "None");
40
+
41
+ export function ReportDetail({ reportId, onDeleted, result: given, onResult, className }: ReportDetailProps) {
42
+ const [confirming, setConfirming] = useState(false);
43
+ const [own, setOwn] = useState<ReportResultData | null>(null);
44
+ const result = given ?? own;
45
+ const setResult = (r: ReportResultData | null) => { setOwn(r); onResult?.(r); };
46
+ const report = useG8Query(async (signal) => reportBody<SavedReport>(await g8Get(`/reports/${encodeURIComponent(reportId)}`, undefined, { signal })), [reportId]);
47
+ const run = useG8Mutation(async () => { const out = reportBody<ReportResultData>(await g8Post(`/reports/${encodeURIComponent(reportId)}/execute`, {})); setResult(out); return out; });
48
+ const remove = useG8Mutation(async () => { await g8Delete(`/reports/${encodeURIComponent(reportId)}`); }, { onSuccess: () => { setConfirming(false); onDeleted?.(); }, onError: () => setConfirming(false) });
49
+
50
+ // A different report starts clean; it runs once on open unless the host already holds a result.
51
+ useEffect(() => { run.reset(); if (!given) void run.mutate(undefined); }, [reportId]); // eslint-disable-line react-hooks/exhaustive-deps
52
+
53
+ const r = report.data;
54
+ const cfg = (r?.config ?? {}) as Cfg;
55
+ if (report.error) return <ApiError error={report.error} title="Could not load the report" onRetry={() => void report.refetch()} />;
56
+ if (!r) return <Skeleton className="h-40 w-full" aria-label="Loading report" />;
57
+
58
+ return (
59
+ <div className={`space-y-4 ${className ?? ""}`} data-testid="report-detail">
60
+ <div className="flex flex-wrap items-center gap-2">
61
+ <StatusPill tone={r.visibility === "private" ? "neutral" : "info"}>{r.visibility}</StatusPill>
62
+ <span className="text-xs text-muted-foreground">Revision {r.revision}</span>
63
+ <div className="ml-auto flex gap-2">
64
+ <Button size="sm" variant="outline" loading={run.loading} onClick={() => void run.mutate(undefined)}>Run again</Button>
65
+ {r.can_delete ? <Button size="sm" variant="ghost" onClick={() => setConfirming(true)}>Delete</Button> : null}
66
+ </div>
67
+ </div>
68
+ <KeyValueList columns={2} items={[
69
+ { label: "Object", value: r.source.plural_noun, mono: false },
70
+ { label: "Object slug", value: r.source.slug, mono: true },
71
+ { label: "Chart", value: r.chart_type },
72
+ { label: "Measure", value: (cfg.measures ?? []).map(measureText).join(", ") || "count" },
73
+ { label: "Grouped by", value: (cfg.group_by ?? []).map(groupText).join(", ") || "None" },
74
+ { label: "Filters", value: (cfg.filters ?? []).length ? `${(cfg.filters ?? []).length} applied` : "None" },
75
+ ]} />
76
+ {run.error ? <ApiError error={run.error} title="Could not run the report" onRetry={() => void run.mutate(undefined)} /> : null}
77
+ {run.loading && !result ? <Skeleton className="h-24 w-full" aria-label="Running report" /> : null}
78
+ {result ? <ResultTable result={result} /> : null}
79
+ {remove.error ? <ApiError error={remove.error} title="Could not delete the report" /> : null}
80
+ <ConfirmSpendDialog open={confirming} tier="destructive" title={`Delete "${r.title}"?`} description="The saved report is removed for everyone who can see it. The underlying records are not touched."
81
+ confirmLabel="Delete report" affected={1} confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setConfirming(false)} />
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ /**
4
+ * TrendChart: a daily line chart for one outbound channel (email, calls or meetings), drawn with the kit TimeSeriesChart (recharts).
5
+ *
6
+ * SDK operations (pick the channel):
7
+ * channel "email": GET /analytics/outbound/email-metrics/trend?days= (analytics.getEmailTrend) read rows { date, sent, replied, ... }
8
+ * channel "calls": GET /analytics/outbound/call-metrics/trend?days= (analytics.getCallTrend) read rows { date, dials, connected, ... }
9
+ * channel "meetings": GET /analytics/outbound/meeting-metrics/trend?days= (analytics.getMeetingTrend) read rows { date, booked, cancelled, held, ... }
10
+ * Two series per channel (the volume and the outcome). Dates are plain `YYYY-MM-DD` days and are shown as UTC days.
11
+ */
12
+ import { Skeleton } from "@graph8/sdk/ui";
13
+ import { ApiError } from "@/g8/components/api-error";
14
+ import { EmptyState } from "@/g8/components/empty-state";
15
+ import { TimeSeriesChart } from "@/g8/components/screen/time-series-chart";
16
+ import { useReport } from "../hooks/use-report";
17
+ import { formatNumber } from "../lib/analytics-types";
18
+
19
+ export type TrendChannel = "email" | "calls" | "meetings";
20
+
21
+ export const TREND_SERIES: Record<TrendChannel, { path: string; keys: [string, string]; labels: [string, string] }> = {
22
+ email: { path: "/analytics/outbound/email-metrics/trend", keys: ["sent", "replied"], labels: ["Sent", "Replied"] },
23
+ calls: { path: "/analytics/outbound/call-metrics/trend", keys: ["dials", "connected"], labels: ["Dials", "Connected"] },
24
+ meetings: { path: "/analytics/outbound/meeting-metrics/trend", keys: ["booked", "held"], labels: ["Booked", "Held"] },
25
+ };
26
+
27
+ export type TrendRow = { date?: string } & Record<string, unknown>;
28
+
29
+ /** Daily values of one key, in date order. */
30
+ export function seriesOf(rows: TrendRow[] | undefined, key: string): number[] {
31
+ return (Array.isArray(rows) ? rows : []).map((r) => Number(r[key] ?? 0) || 0);
32
+ }
33
+
34
+ export interface TrendChartProps {
35
+ channel?: TrendChannel;
36
+ days?: number;
37
+ className?: string;
38
+ }
39
+
40
+ export function TrendChart({ channel = "email", days = 30, className }: TrendChartProps) {
41
+ const spec = TREND_SERIES[channel];
42
+ const { data, loading, error, refetch } = useReport<TrendRow[]>(spec.path, { days });
43
+ const rows = Array.isArray(data) ? data : [];
44
+ const a = seriesOf(rows, spec.keys[0]);
45
+ const b = seriesOf(rows, spec.keys[1]);
46
+ const total = (v: number[]) => v.reduce((x, y) => x + y, 0);
47
+
48
+ if (error) return <ApiError error={error} title="Could not load the trend" onRetry={() => void refetch()} />;
49
+ if (loading && !data) return <Skeleton className="h-48 w-full" aria-label="Loading trend" />;
50
+ if (rows.length === 0 || total(a) === 0) return <EmptyState className={className} title="No activity in this window" description="Daily numbers appear here as emails, calls and meetings are recorded." />;
51
+
52
+ return (
53
+ <figure className={className} aria-label={`${channel} trend`}>
54
+ <div className="mb-2 flex flex-wrap gap-4 text-xs text-muted-foreground">
55
+ <span>{spec.labels[0]} {formatNumber(total(a))}</span>
56
+ <span>{spec.labels[1]} {formatNumber(total(b))}</span>
57
+ </div>
58
+ <TimeSeriesChart data={rows} xKey="date" kind="line" height={200}
59
+ series={[{ key: spec.keys[0], label: spec.labels[0] }, { key: spec.keys[1], label: spec.labels[1], tone: "positive" }]} />
60
+ </figure>
61
+ );
62
+ }
@@ -113,8 +113,12 @@
113
113
  "name": "MeetingsScreen",
114
114
  "file": "components/meetings-screen.tsx",
115
115
  "purpose": "Finished meetings product: upcoming, past, cancelled and recorded meetings with detail, approvals, notes and a schedule-a-meeting flow.",
116
- "operations": [],
117
- "tiers": []
116
+ "operations": [
117
+ "list_bookings_appointments_bookings_get"
118
+ ],
119
+ "tiers": [
120
+ "read"
121
+ ]
118
122
  },
119
123
  {
120
124
  "name": "SchedulingScreen",
@@ -23,6 +23,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
23
23
  import { useG8Query } from "@/g8/hooks/use-g8-query";
24
24
  import { useSchedules } from "../hooks/use-schedules";
25
25
  import { WEEKDAYS, listOf, toHHMM, unwrap, type AvailabilityRule, type Schedule, type TimeWindow } from "../lib/appointments-types";
26
+ import { useBrand } from "@/g8/hooks/use-brand";
26
27
 
27
28
  export interface AvailabilityEditorProps {
28
29
  /** Schedule to edit; defaults to the org's default schedule. */
@@ -72,6 +73,7 @@ function WindowRow({ window: w, onChange, onRemove }: { window: TimeWindow; onCh
72
73
  }
73
74
 
74
75
  function CalendarPanel({ connectUrl }: { connectUrl: string }) {
76
+ const brand = useBrand();
75
77
  const cals = useG8Query(async (signal) => listOf<CalendarRow>(await g8Get("/appointments/calendars", undefined, { signal }), "calendars"), []);
76
78
  return (
77
79
  <section aria-label="Calendars" className="space-y-2 rounded-lg border p-3 text-sm">
@@ -84,22 +86,26 @@ function CalendarPanel({ connectUrl }: { connectUrl: string }) {
84
86
  ))}
85
87
  {!cals.loading && !cals.error && (cals.data ?? []).length === 0 ? <p className="text-muted-foreground">No calendar connected, so busy time is not checked.</p> : null}
86
88
  <a className="underline" href={connectUrl} target="_blank" rel="noreferrer">
87
- Connect or manage calendars in graph8
89
+ Connect or manage calendars in {brand.linkLabel}
88
90
  </a>
89
91
  </section>
90
92
  );
91
93
  }
92
94
 
93
- export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://app.graph8.com", onSaved, className }: AvailabilityEditorProps) {
95
+ export function AvailabilityEditor({ scheduleId, connectCalendarUrl: connectCalendarUrlProp, onSaved, className }: AvailabilityEditorProps) {
96
+ const brand = useBrand();
97
+ const connectCalendarUrl = connectCalendarUrlProp ?? brand.appUrl;
94
98
  const { schedules, loading: loadingSchedules, error: schedulesError } = useSchedules();
95
99
  const [picked, setPicked] = useState<number | undefined>(scheduleId);
96
100
  const activeId = picked ?? schedules[0]?.id;
97
101
  const [week, setWeek] = useState<Week>({});
98
102
  const [overrides, setOverrides] = useState<Override[]>([]);
99
103
  const [newDate, setNewDate] = useState("");
104
+ /** The schedule whose hours are in the form. Save is offered only for a schedule that has loaded, never over a blank week. */
105
+ const [appliedFor, setAppliedFor] = useState<number | undefined>(undefined);
100
106
 
101
107
  // Stale context: switching schedule must not show (or save!) the previous schedule's hours.
102
- useEffect(() => { setWeek({}); setOverrides([]); }, [activeId]);
108
+ useEffect(() => { setWeek({}); setOverrides([]); setAppliedFor(undefined); }, [activeId]);
103
109
 
104
110
  const availability = useG8Query(
105
111
  async (signal) => unwrap<Schedule>(await g8Get(`/appointments/schedules/${activeId}/availability`, undefined, { signal })),
@@ -109,9 +115,10 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
109
115
  useEffect(() => {
110
116
  const data = availability.data;
111
117
  if (!data) return;
118
+ setAppliedFor(activeId);
112
119
  setWeek(toWeek(data.availability ?? []));
113
120
  setOverrides((data.overrides ?? []).filter((o) => o.date).map((o) => ({ date: o.date as string, start: toHHMM(o.start_time), end: toHHMM(o.end_time, "17:00") })));
114
- }, [availability.data]);
121
+ }, [availability.data, activeId]);
115
122
 
116
123
  const save = useG8Mutation(
117
124
  async () =>
@@ -122,10 +129,11 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
122
129
  { onSuccess: () => onSaved?.() },
123
130
  );
124
131
 
132
+ const ready = appliedFor === activeId && !availability.error && !availability.loading;
125
133
  const setDay = (day: number, windows: TimeWindow[]) => setWeek((w) => ({ ...w, [day]: windows }));
126
134
  if (schedulesError) return <ApiError error={schedulesError} title="Could not load schedules" />;
127
135
  if (loadingSchedules) return <Skeleton className="h-64 w-full" aria-label="Loading schedules" />;
128
- if (activeId == null) return <EmptyState title="No schedule yet" description="Create a working-hours schedule in graph8 (or POST /appointments/schedules), then edit it here." />;
136
+ if (activeId == null) return <EmptyState title="No schedule yet" description={`Create a working-hours schedule in ${brand.linkLabel} (or POST /appointments/schedules), then edit it here.`} />;
129
137
 
130
138
  return (
131
139
  <div className={`space-y-4 ${className ?? ""}`}>
@@ -141,6 +149,8 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
141
149
  <p className="text-sm text-muted-foreground">Times are wall-clock hours in the schedule's time zone: {schedules.find((x) => x.id === activeId)?.time_zone ?? "the schedule's zone"}.</p>
142
150
  {availability.error ? <ApiError error={availability.error} title="Could not load availability" onRetry={() => void availability.refetch()} /> : null}
143
151
 
152
+ {!ready ? (availability.error ? null : <Skeleton className="h-48 w-full" aria-label="Loading availability" />) : (
153
+ <>
144
154
  <div className="space-y-2">
145
155
  {WEEKDAYS.map(({ day, label }) => {
146
156
  const windows = week[day] ?? [];
@@ -186,6 +196,8 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
186
196
  <Button onClick={() => void save.mutate(undefined)} loading={save.loading}>
187
197
  Save availability
188
198
  </Button>
199
+ </>
200
+ )}
189
201
  <CalendarPanel connectUrl={connectCalendarUrl} />
190
202
  </div>
191
203
  );
@@ -116,7 +116,8 @@ export function EventTypeEditor({ eventTypeId, onSaved, onDeleted, className }:
116
116
  const valid = form.title.trim().length > 0 && form.length >= 5 && (slugTouched ? form.slug.trim().length > 0 : true);
117
117
 
118
118
  if (existing.error) return <ApiError error={existing.error} title="Could not load the event type" onRetry={() => void existing.refetch()} />;
119
- if (eventTypeId != null && existing.loading && !existing.data) return <Skeleton className="h-64 w-full" aria-label="Loading event type" />;
119
+ // Also while the NEXT event type loads (the hook keeps the previous one's data meanwhile): the form is blank then, and saving it would blank the record.
120
+ if (eventTypeId != null && (existing.loading || !existing.data)) return <Skeleton className="h-64 w-full" aria-label="Loading event type" />;
120
121
 
121
122
  return (
122
123
  <form
@@ -21,6 +21,8 @@ import { g8Get } from "@/g8/lib/g8-client";
21
21
  import { useG8Query } from "@/g8/hooks/use-g8-query";
22
22
  import { formatDateTime, listOf, unwrap, type Booking, type EventType } from "../lib/appointments-types";
23
23
  import { MeetingDetail } from "./meeting-detail";
24
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
25
+ import { RecordLink } from "@/g8/components/screen/record-link";
24
26
 
25
27
  export function EventTypeOverview({ eventTypeId }: { eventTypeId: number }) {
26
28
  const q = useG8Query(async (signal) => unwrap<EventType>(await g8Get(`/appointments/event-types/${eventTypeId}`, undefined, { signal })), [eventTypeId]);
@@ -52,8 +54,8 @@ export function EventTypeBookings({ eventTypeId, pageSize = 20 }: { eventTypeId:
52
54
  <TableHeader><TableRow><TableHead>When</TableHead><TableHead>Attendees</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
53
55
  <TableBody>
54
56
  {q.data.map((b) => (
55
- <TableRow key={b.uid} className="cursor-pointer" onClick={() => setOpen(b.uid)}>
56
- <TableCell><button type="button" className="font-medium hover:underline" onClick={(e) => { e.stopPropagation(); setOpen(b.uid); }}>{formatDateTime(b.start_time)}</button></TableCell>
57
+ <TableRow key={b.uid} className="cursor-pointer" {...rowActivationProps(() => setOpen(b.uid))}>
58
+ <TableCell><RecordLink id={b.uid} onOpen={() => setOpen(b.uid)}>{formatDateTime(b.start_time)}</RecordLink></TableCell>
57
59
  <TableCell className="max-w-64 truncate">{b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees"}</TableCell>
58
60
  <TableCell><StatusPill tone={toneForStatus(b.status)} dot>{humanizeStatus(b.status)}</StatusPill></TableCell>
59
61
  </TableRow>
@@ -19,11 +19,15 @@ import { EmptyState } from "@/g8/components/empty-state";
19
19
  import { g8Get } from "@/g8/lib/g8-client";
20
20
  import { useG8Query } from "@/g8/hooks/use-g8-query";
21
21
  import { formatDateTime, listOf, type Booking, type RecordedMeeting } from "../lib/appointments-types";
22
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
23
+ import { RecordLink } from "@/g8/components/screen/record-link";
22
24
 
23
25
  export type MeetingView = "upcoming" | "past" | "cancelled" | "recorded";
24
26
  export type MeetingSelection = { kind: "booking"; id: string } | { kind: "meeting"; id: string };
25
27
 
26
28
  export interface MeetingListProps {
29
+ /** 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. */
30
+ linkParam?: string;
27
31
  onSelect?: (selection: MeetingSelection) => void;
28
32
  initialView?: MeetingView;
29
33
  pageSize?: number;
@@ -58,7 +62,7 @@ function useDebouncedText(value: string, ms = 300): string {
58
62
  return v;
59
63
  }
60
64
 
61
- export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
65
+ export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
62
66
  const [view, setView] = useState<MeetingView>(initialView);
63
67
  const [page, setPage] = useState(0);
64
68
  const [search, setSearch] = useState("");
@@ -120,17 +124,17 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
120
124
  <TableBody>
121
125
  {result.kind === "booking"
122
126
  ? (result.items as Booking[]).map((b) => (
123
- <TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer" onClick={() => onSelect?.({ kind: "booking", id: b.uid })}>
127
+ <TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "booking", id: b.uid })) : {})}>
124
128
  <TableCell className="whitespace-nowrap">{formatDateTime(b.start_time)}</TableCell>
125
- <TableCell className="font-medium">{b.title || b.event_type_title || "Meeting"}</TableCell>
129
+ <TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={`booking:${b.uid}`} onOpen={() => onSelect({ kind: "booking", id: b.uid })}>{b.title || b.event_type_title || "Meeting"}</RecordLink> : b.title || b.event_type_title || "Meeting"}</TableCell>
126
130
  <TableCell className="max-w-64 truncate">{attendeeNames(b)}</TableCell>
127
131
  <TableCell className="capitalize">{b.status}</TableCell>
128
132
  </TableRow>
129
133
  ))
130
134
  : (result.items as RecordedMeeting[]).map((m) => (
131
- <TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer" onClick={() => onSelect?.({ kind: "meeting", id: m.id })}>
135
+ <TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "meeting", id: m.id })) : {})}>
132
136
  <TableCell className="whitespace-nowrap">{formatDateTime(m.start_time)}</TableCell>
133
- <TableCell className="font-medium">{m.subject || "Untitled meeting"}</TableCell>
137
+ <TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={`meeting:${m.id}`} onOpen={() => onSelect({ kind: "meeting", id: m.id })}>{m.subject || "Untitled meeting"}</RecordLink> : m.subject || "Untitled meeting"}</TableCell>
134
138
  <TableCell className="max-w-64 truncate">{m.transcript_summary || "No summary yet"}</TableCell>
135
139
  <TableCell className="capitalize">{m.transcript_status ?? "none"}</TableCell>
136
140
  </TableRow>
@@ -40,6 +40,7 @@ import { listOf, type Booking } from "../lib/appointments-types";
40
40
  import { BookingPage } from "./booking-page";
41
41
  import { MeetingDetail } from "./meeting-detail";
42
42
  import { MeetingList, type MeetingSelection } from "./meeting-list";
43
+ import { useBrand } from "@/g8/hooks/use-brand";
43
44
 
44
45
  export interface MeetingsScreenProps {
45
46
  /** Where a person connects a calendar (graph8 app). */
@@ -99,7 +100,9 @@ function ScheduleSheet({ open, onOpenChange, onBooked }: { open: boolean; onOpen
99
100
  );
100
101
  }
101
102
 
102
- export function MeetingsScreen({ connectCalendarUrl = "https://app.graph8.com", renderDetailActions, extraDetailTabs, toolbarActions }: MeetingsScreenProps) {
103
+ export function MeetingsScreen({ connectCalendarUrl: connectCalendarUrlProp, renderDetailActions, extraDetailTabs, toolbarActions }: MeetingsScreenProps) {
104
+ const brand = useBrand();
105
+ const connectCalendarUrl = connectCalendarUrlProp ?? brand.appUrl;
103
106
  const [raw, setRaw] = useUrlState("sel");
104
107
  const [mode, setMode] = useUrlState("view");
105
108
  const selection = parseSelection(raw);
@@ -143,10 +146,10 @@ export function MeetingsScreen({ connectCalendarUrl = "https://app.graph8.com",
143
146
  expanded={mode === "full" && Boolean(selection)}
144
147
  onExpandedChange={(full) => setMode(full ? "full" : null)}
145
148
  detail={detail}
146
- list={<MeetingList refreshKey={version} selectedId={selection?.id ?? null} onSelect={(s) => setRaw(`${s.kind}:${s.id}`)} />}
149
+ list={<MeetingList linkParam="sel" refreshKey={version} selectedId={selection?.id ?? null} onSelect={(s) => setRaw(`${s.kind}:${s.id}`)} />}
147
150
  />
148
- <SectionCard title="Calendar sync" description="Google and Microsoft sign-in happens in the graph8 app. Once connected, booked and recorded meetings appear here.">
149
- <a className="inline-flex items-center gap-1 text-sm underline" href={connectCalendarUrl} target="_blank" rel="noreferrer">Connect a calendar in graph8<ExternalLink className="size-3.5" aria-hidden /></a>
151
+ <SectionCard title="Calendar sync" description={`Google and Microsoft sign-in happens in the ${brand.linkLabel} app. Once connected, booked and recorded meetings appear here.`}>
152
+ <a className="inline-flex items-center gap-1 text-sm underline" href={connectCalendarUrl} target="_blank" rel="noreferrer">Connect a calendar in {brand.linkLabel}<ExternalLink className="size-3.5" aria-hidden /></a>
150
153
  </SectionCard>
151
154
  <ScheduleSheet open={scheduling} onOpenChange={setScheduling} onBooked={changed} />
152
155
  </ScreenLayout>
@@ -37,6 +37,7 @@ import { AvailabilityEditor } from "./availability-editor";
37
37
  import { BookingPage } from "./booking-page";
38
38
  import { EventTypeEditor } from "./event-type-editor";
39
39
  import { EventTypeBookings, EventTypeOverview } from "./event-type-related";
40
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
40
41
 
41
42
  export interface SchedulingScreenProps {
42
43
  /** Where a person connects a calendar (graph8 app). */
@@ -88,7 +89,7 @@ export function SchedulingScreen({ connectCalendarUrl, renderRowActions, renderD
88
89
  <TableHeader><TableRow><TableHead>Event type</TableHead><TableHead className="text-right">Length</TableHead><TableHead>Rules</TableHead>{renderRowActions ? <TableHead /> : null}</TableRow></TableHeader>
89
90
  <TableBody>
90
91
  {types.eventTypes.map((t) => (
91
- <TableRow key={t.id} className="cursor-pointer" data-state={String(t.id) === idParam ? "selected" : undefined} onClick={() => setId(String(t.id))}>
92
+ <TableRow key={t.id} className="cursor-pointer" data-state={String(t.id) === idParam ? "selected" : undefined} {...rowActivationProps(() => setId(String(t.id)))}>
92
93
  <TableCell><button type="button" className="font-medium hover:underline" onClick={(e) => { e.stopPropagation(); setId(String(t.id)); }}>{t.title}</button><div className="text-xs text-muted-foreground">/{t.slug}</div></TableCell>
93
94
  <TableCell className="text-right tabular-nums">{t.length} min</TableCell>
94
95
  <TableCell className="space-x-1">
@@ -36,6 +36,24 @@
36
36
  }
37
37
  }
38
38
  },
39
+ "screens": {
40
+ "type": "array",
41
+ "description": "Optional. How the block's finished screens become routes and sidebar entries in an app made with `create --blocks`. Absent: derived from the components named `*Screen` (ContactsScreen -> /contacts, label Contacts, group = the block title, icon from a built-in per-block map).",
42
+ "items": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["component", "path", "label"],
46
+ "properties": {
47
+ "component": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$", "description": "A component of this block." },
48
+ "path": { "type": "string", "pattern": "^/[a-z0-9]+(-[a-z0-9]+)*(/[a-z0-9]+(-[a-z0-9]+)*)*$", "description": "Static route path. On a collision in a generated app it is prefixed with the block id." },
49
+ "label": { "type": "string", "minLength": 1 },
50
+ "icon": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$", "description": "A lucide-react export name." },
51
+ "group": { "type": "string", "minLength": 1, "description": "Sidebar section (default: the block title)." },
52
+ "description": { "type": "string", "minLength": 1 },
53
+ "order": { "type": "number", "description": "Sort position inside the block; lower first." }
54
+ }
55
+ }
56
+ },
39
57
  "hooks": {
40
58
  "type": "array",
41
59
  "items": {
@@ -59,6 +59,21 @@ export interface BlockDefault {
59
59
  askHuman: boolean;
60
60
  }
61
61
 
62
+ /** Optional route metadata for a finished screen (`create --blocks`, `add` inside a created app). */
63
+ export interface BlockScreen {
64
+ /** A component of this block. */
65
+ component: string;
66
+ /** Static route path; prefixed with the block id when it collides in a generated app. */
67
+ path: string;
68
+ label: string;
69
+ /** lucide-react export name. */
70
+ icon?: string;
71
+ /** Sidebar section (default: the block title). */
72
+ group?: string;
73
+ description?: string;
74
+ order?: number;
75
+ }
76
+
62
77
  export interface BlockManifest {
63
78
  $schema?: string;
64
79
  id: string;
@@ -71,6 +86,8 @@ export interface BlockManifest {
71
86
  /** Contract operationIds (keys of API_OPERATIONS). */
72
87
  operations: string[];
73
88
  components: BlockComponent[];
89
+ /** Optional. Absent: derived from the components named `*Screen`. */
90
+ screens?: BlockScreen[];
74
91
  hooks: BlockHook[];
75
92
  lib?: BlockFile[];
76
93
  server?: BlockServerFile[];
@@ -7,6 +7,26 @@ Apollo / HubSpot "campaigns" screen, with generation behind credit confirmations
7
7
  Depends on: `lists` (the audience picker), `mailboxes` (senders and the connect-a-mailbox link). Install:
8
8
  `npx @graph8/sdk add campaigns`.
9
9
 
10
+ ## Screens
11
+
12
+ `CampaignsScreen` is the finished page: mount it as the route. Every record type has a complete detail view,
13
+ deep-linkable (`?tab=campaigns|profile|personas`, `?id=` the record, `?dtab=` the tab, `?doc=` a document).
14
+
15
+ - **Campaigns tab**: list (delete confirmed) beside a detail panel with Details (every field, audience, edit name,
16
+ goal and brief, confirmed), Metrics, Documents (each document opens with its fields and editor), Audience,
17
+ Sequence and Launch. "New campaign" runs the five-step wizard in a sheet.
18
+ - **Company profile tab**: the profile plus ICPs; `IcpDetail` shows every field and edits, pins or archives.
19
+ - **Personas tab**: `PersonaDetail` shows every field, the campaigns that target it, and edits, pins or archives.
20
+
21
+ Slot props: `toolbarActions`, `renderDetailActions(campaign)`, `extraDetailTabs`, `websiteUrl`. Prerequisites: a
22
+ company profile with the real website, an active mailbox, a contact list, and credits for AI generation.
23
+ `IcpEditor` and `PersonaEditor` gained optional `onOpen` and `refreshKey`; `CampaignAssets` optional
24
+ `documentId` and `onDocumentChange`.
25
+
26
+ Known limits: there is no single-ICP read, so `IcpDetail` finds the ICP in the first 200 (the API maximum).
27
+ `PersonaDetail` lists the campaigns whose `target_persona` matches the persona title among the first 100
28
+ campaigns; the API does not link campaigns to ICPs, so ICPs show none.
29
+
10
30
  ## Components
11
31
 
12
32
  | Component | What it does | Tiers |
@@ -16,7 +36,7 @@ Depends on: `lists` (the audience picker), `mailboxes` (senders and the connect-
16
36
  | `CampaignWizard` | A page. Idea (AI, polled) -> create -> audience -> steps -> launch. Also exports the three step components below. | all |
17
37
  | `CampaignAudienceStep` | Attach or detach a contact list (uses the lists block `ListPicker`; reads `getCampaignFull`, since `getCampaign` omits the audience). | read, write, external |
18
38
  | `CampaignSequenceStep` | Add, edit, remove and AI-write sequence steps. | read, billable, external, destructive |
19
- | `CampaignLaunchStep` | Readiness checklist, sender choice, deliverability warning, confirmed launch. | read, external |
39
+ | `CampaignLaunchStep` | Readiness checklist (audience, steps, written copy, sender), deliverability and warm-up hold warnings, writes missing email copy with AI, confirmed launch. | read, billable, external |
20
40
  | `CampaignList` | A page. All campaigns; open one for metrics and documents; delete. | read, billable, external, destructive |
21
41
  | `CampaignAssets` | Campaign documents: read, edit, rewrite with AI, add, delete. Standalone: shows a campaign picker. | read, billable, external, destructive |
22
42
  | `CampaignMetrics` | A page. Metrics for 7, 30, 90 or 365 days. Standalone: shows a campaign picker. | read |
@@ -40,15 +60,24 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
40
60
  3. **Ideas.** `generateCampaignIdeasAsync` (billable) -> `task_id` -> poll `getCampaignIdeasTaskStatus` every
41
61
  2-3 seconds until `completed`; `ideas[]` carry `campaign_name`, `core_concept`, `primary_hook`.
42
62
  4. **Create.** `createCampaign` -> `id`. Store it at once; later steps reuse it and "Create" becomes
43
- "Save changes" (`updateCampaign`). With `auto_generate_documents` the documents generate in the
44
- background: poll `getCampaignGenerationStatus`.
63
+ "Save changes" (`updateCampaign`). With `auto_generate_documents` (on by default in the wizard and the
64
+ recipe) the documents generate in the background: poll `getCampaignGenerationStatus`; a `generation_error`
65
+ there, or `generation_status: "failed_to_dispatch"` on create, means the writing never started.
45
66
  5. **Audience.** `getCampaignFull` shows what is attached (`audience_list_id`, `audience`). `ListPicker` (lists block) -> a list `id` -> `attachAudienceToCampaign` with `list_id` as a
46
67
  string. Build the list in the search/lists blocks first.
47
68
  6. **Steps and copy.** `getCampaignSequence` -> `createCampaignStep` (channel -> mode matrix: email -> email,
48
69
  phone -> call|voicemail, sms -> sms, social -> connect_note|dm|comment|reply) -> `generateCampaignStepContent`
49
- (billable) per step. `CampaignAssets` edits the documents behind them.
50
- 7. **Launch.** `CampaignLaunchStep` -> `getCampaignFull` (audience attached with its size, >= 1 step, `is_launched`),
51
- `listMailboxes` (active ones), `getSafeToSend` (a warning) -> `launchCampaign` with `sender_mailbox_ids`.
70
+ (billable) per step. `CampaignAssets` edits the documents behind them. Writing an EMAIL step needs the
71
+ campaign's `emails` document to exist already (created by `auto_generate_documents`); without it the step
72
+ write answers 404.
73
+ 7. **Launch.** `CampaignLaunchStep` -> `getCampaignFull` (audience attached with its size, >= 1 step, written copy,
74
+ `is_launched`), `listMailboxes` (active ones), `getSafeToSend` (`safe`, a warning) -> `launchCampaign` with
75
+ `sender_mailbox_ids`. The launch is refused (400 "no completed copy documents") unless a document other than
76
+ `sequence` and `step_catalog` has `status: "completed"`, so Launch stays disabled until one does; "Write the
77
+ email copy with AI" creates the `emails` document if missing (`createCampaignDocument`, once) and has AI write it
78
+ (`regenerateCampaignDocument`, billable). `is_launched` is true only for a live launch, so a failed launch can be
79
+ retried. A mailbox added less than 14 days ago is in its warm-up hold: the campaign goes live but sends nothing
80
+ from it until the hold ends (the step warns).
52
81
  The one-step launch executes email only; configure other channels in the sequencer block.
53
82
  8. **Measure.** `CampaignMetrics` -> `getCampaignMetrics` with `days`.
54
83
 
@@ -62,8 +91,9 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
62
91
 
63
92
  ## Defaults (do not ask the human)
64
93
 
65
- - 5 ideas, goal `awareness`, channel `email`, no auto-generated documents (generate per step so each spend is
66
- confirmed), sender = the first active mailbox.
94
+ - 5 ideas, goal `awareness`, channel `email`, AI-written documents ON (a campaign without written copy cannot
95
+ launch; the create confirmation shows the spend), personas picked by AI when none are chosen
96
+ (`persona_mode: "ai_pick"`), sender = the first active mailbox.
67
97
  - **Ask the human**: whether to launch and to which list (state the recipient count), and whether to approve
68
98
  (lock) the company profile.
69
99
 
@@ -95,7 +125,7 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
95
125
  | `get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get` | GET /campaigns/ideas/status/{task_id} | read | CampaignWizard |
96
126
  | `create_campaign_campaigns_post` | POST /campaigns | external | CampaignWizard |
97
127
  | `update_campaign_campaigns__campaign_id__patch` | PATCH /campaigns/{campaign_id} | external | CampaignWizard |
98
- | `get_campaign_generation_status_campaigns__campaign_id__status_get` | GET /campaigns/{campaign_id}/status | read | CampaignWizard |
128
+ | `get_campaign_generation_status_campaigns__campaign_id__status_get` | GET /campaigns/{campaign_id}/status | read | CampaignWizard, CampaignLaunchStep |
99
129
  | `get_campaign_full_campaigns__campaign_id__full_get` | GET /campaigns/{campaign_id}/full | read | CampaignWizard, CampaignAudienceStep, CampaignLaunchStep |
100
130
  | `attach_audience_to_campaign_campaigns__campaign_id__audience_put` | PUT /campaigns/{campaign_id}/audience | external | CampaignWizard, CampaignAudienceStep |
101
131
  | `get_campaign_sequence_campaigns__campaign_id__sequence_get` | GET /campaigns/{campaign_id}/sequence | read | CampaignWizard, CampaignSequenceStep |
@@ -112,7 +142,7 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
112
142
  | `get_campaign_metrics_campaigns__campaign_id__metrics_get` | GET /campaigns/{campaign_id}/metrics | read | CampaignList, CampaignMetrics |
113
143
  | `list_campaign_documents_campaigns__campaign_id__documents_get` | GET /campaigns/{campaign_id}/documents | read | CampaignList, CampaignAssets |
114
144
  | `get_campaign_document_campaigns__campaign_id__documents__document_id__get` | GET /campaigns/{campaign_id}/documents/{document_id} | read | CampaignList, CampaignAssets |
115
- | `create_campaign_document_campaigns__campaign_id__documents_post` | POST /campaigns/{campaign_id}/documents | external | CampaignList, CampaignAssets |
145
+ | `create_campaign_document_campaigns__campaign_id__documents_post` | POST /campaigns/{campaign_id}/documents | external | CampaignWizard, CampaignLaunchStep, CampaignList, CampaignAssets |
116
146
  | `update_campaign_document_campaigns__campaign_id__documents__document_id__put` | PUT /campaigns/{campaign_id}/documents/{document_id} | external | CampaignList, CampaignAssets |
117
147
  | `delete_campaign_document_campaigns__campaign_id__documents__document_id__delete` | DELETE /campaigns/{campaign_id}/documents/{document_id} | destructive | CampaignList, CampaignAssets |
118
- | `regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post` | POST /campaigns/{campaign_id}/documents/{document_id}/regenerate | billable | CampaignList, CampaignAssets |
148
+ | `regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post` | POST /campaigns/{campaign_id}/documents/{document_id}/regenerate | billable | CampaignWizard, CampaignLaunchStep, CampaignList, CampaignAssets |