@graph8/sdk 0.362.0 → 0.373.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (615) hide show
  1. package/README.md +33 -10
  2. package/blocks/_foundation/README.md +78 -1
  3. package/blocks/_foundation/block.json +123 -6
  4. package/blocks/_foundation/components/api-error.tsx +9 -7
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  7. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  8. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  9. package/blocks/_foundation/components/empty-state.tsx +5 -2
  10. package/blocks/_foundation/components/list-select.tsx +112 -0
  11. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  12. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  13. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  14. package/blocks/_foundation/components/screen/index.ts +2 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  17. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  18. package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
  19. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  20. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  21. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  22. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  23. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  24. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  25. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  26. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  27. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  28. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  29. package/blocks/_foundation/lib/g8-client.ts +1 -1
  30. package/blocks/_foundation/lib/paging.ts +57 -0
  31. package/blocks/_foundation/lib/plural.ts +23 -0
  32. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  33. package/blocks/_foundation/server/connect-route.ts +15 -0
  34. package/blocks/_foundation/server/connect.ts +127 -0
  35. package/blocks/_foundation/server/local-key.ts +70 -0
  36. package/blocks/_foundation/server/proxy.ts +103 -3
  37. package/blocks/_foundation/server/route.ts +16 -4
  38. package/blocks/agents/README.md +93 -2
  39. package/blocks/agents/block.json +343 -2
  40. package/blocks/agents/components/agent-activity.tsx +112 -0
  41. package/blocks/agents/components/agent-approvals.tsx +135 -0
  42. package/blocks/agents/components/agent-builder.tsx +29 -10
  43. package/blocks/agents/components/agent-conversations.tsx +143 -0
  44. package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
  45. package/blocks/agents/components/agent-operators.tsx +164 -0
  46. package/blocks/agents/components/agent-run-detail.tsx +119 -0
  47. package/blocks/agents/components/agent-runs.tsx +19 -10
  48. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  49. package/blocks/agents/components/agent-test-history.tsx +121 -0
  50. package/blocks/agents/components/agents-screen.tsx +226 -0
  51. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  52. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  53. package/blocks/agents/components/from-template-button.tsx +65 -0
  54. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  55. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  56. package/blocks/agents/components/knowledge-form.tsx +69 -0
  57. package/blocks/agents/components/name-dialog.tsx +32 -0
  58. package/blocks/agents/components/receptionist-button.tsx +46 -0
  59. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  60. package/blocks/agents/components/skill-editor.tsx +147 -0
  61. package/blocks/agents/components/skill-library.tsx +111 -0
  62. package/blocks/agents/lib/agent-types.ts +7 -1
  63. package/blocks/agents/lib/facts.tsx +13 -0
  64. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  65. package/blocks/agents/lib/load-more.tsx +141 -0
  66. package/blocks/agents/lib/skill-utils.ts +91 -0
  67. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  68. package/blocks/analytics/README.md +13 -0
  69. package/blocks/analytics/block.json +45 -1
  70. package/blocks/analytics/components/analytics-screen.tsx +222 -0
  71. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  72. package/blocks/analytics/components/leaderboard.tsx +14 -7
  73. package/blocks/analytics/components/report-builder.tsx +38 -11
  74. package/blocks/analytics/components/report-detail.tsx +84 -0
  75. package/blocks/analytics/components/trend-chart.tsx +62 -0
  76. package/blocks/appointments/README.md +6 -0
  77. package/blocks/appointments/block.json +33 -2
  78. package/blocks/appointments/components/availability-editor.tsx +17 -5
  79. package/blocks/appointments/components/booking-page.tsx +2 -2
  80. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  81. package/blocks/appointments/components/event-type-related.tsx +4 -2
  82. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  83. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  84. package/blocks/appointments/components/meeting-list.tsx +38 -18
  85. package/blocks/appointments/components/meetings-screen.tsx +7 -4
  86. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  87. package/blocks/appointments/components/scheduling-screen.tsx +2 -1
  88. package/blocks/block.schema.json +18 -0
  89. package/blocks/block.types.ts +17 -0
  90. package/blocks/campaigns/README.md +41 -11
  91. package/blocks/campaigns/block.json +105 -42
  92. package/blocks/campaigns/components/campaign-assets.tsx +17 -6
  93. package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
  94. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  95. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  96. package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
  97. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  98. package/blocks/campaigns/components/icp-detail.tsx +163 -0
  99. package/blocks/campaigns/components/icp-editor.tsx +22 -17
  100. package/blocks/campaigns/components/persona-detail.tsx +181 -0
  101. package/blocks/campaigns/components/persona-editor.tsx +19 -11
  102. package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
  103. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  104. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  105. package/blocks/coach/README.md +10 -0
  106. package/blocks/coach/block.json +16 -1
  107. package/blocks/coach/components/call-scorecard.tsx +7 -7
  108. package/blocks/coach/components/coach-screen.tsx +222 -0
  109. package/blocks/company-intelligence/README.md +132 -0
  110. package/blocks/company-intelligence/block.json +318 -0
  111. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  112. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  113. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  114. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  115. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  116. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  117. package/blocks/company-intelligence/components/research-run.tsx +161 -0
  118. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  119. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  120. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  121. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  122. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  123. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  124. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  125. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  126. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  127. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  128. package/blocks/copilot/README.md +13 -0
  129. package/blocks/copilot/block.json +14 -1
  130. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  131. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  132. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  133. package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
  134. package/blocks/crm/README.md +34 -0
  135. package/blocks/crm/block.json +107 -2
  136. package/blocks/crm/components/companies-screen.tsx +359 -0
  137. package/blocks/crm/components/company-detail.tsx +10 -8
  138. package/blocks/crm/components/company-table.tsx +11 -4
  139. package/blocks/crm/components/contact-detail.tsx +10 -7
  140. package/blocks/crm/components/contact-lookup-link.tsx +44 -0
  141. package/blocks/crm/components/contact-table.tsx +10 -3
  142. package/blocks/crm/components/contacts-screen.tsx +377 -0
  143. package/blocks/crm/components/dedupe-review.tsx +13 -6
  144. package/blocks/crm/components/import-wizard.tsx +7 -9
  145. package/blocks/crm/components/list-select.tsx +20 -0
  146. package/blocks/crm/components/notes-panel.tsx +4 -4
  147. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  148. package/blocks/crm/components/record-form.tsx +6 -4
  149. package/blocks/crm/components/task-list.tsx +4 -4
  150. package/blocks/crm/components/tasks-screen.tsx +399 -0
  151. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  152. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  153. package/blocks/crm/hooks/use-page-query.ts +9 -1
  154. package/blocks/crm/lib/crm-types.ts +26 -2
  155. package/blocks/crm/lib/read-lists.ts +16 -0
  156. package/blocks/crm/lib/screen-parts.tsx +105 -0
  157. package/blocks/deals/README.md +24 -6
  158. package/blocks/deals/block.json +160 -5
  159. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  160. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  161. package/blocks/deals/components/deal-detail.tsx +25 -13
  162. package/blocks/deals/components/deal-filters.tsx +65 -0
  163. package/blocks/deals/components/deal-line-items.tsx +120 -0
  164. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  165. package/blocks/deals/components/deal-related.tsx +31 -10
  166. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  167. package/blocks/deals/components/deals-screen.tsx +66 -26
  168. package/blocks/deals/components/forecast-view.tsx +4 -4
  169. package/blocks/deals/components/pipeline-board.tsx +8 -6
  170. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  171. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  172. package/blocks/deals/components/stage-editor.tsx +5 -5
  173. package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
  174. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  175. package/blocks/deals/lib/deal-extras.ts +156 -0
  176. package/blocks/dialer/README.md +45 -4
  177. package/blocks/dialer/block.json +84 -3
  178. package/blocks/dialer/components/call-contact-button.tsx +269 -0
  179. package/blocks/dialer/components/call-log-screen.tsx +208 -0
  180. package/blocks/dialer/components/call-log.tsx +23 -7
  181. package/blocks/dialer/components/call-queue.tsx +4 -4
  182. package/blocks/dialer/components/dialer-screen.tsx +361 -0
  183. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  184. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  185. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  186. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  187. package/blocks/dialer/components/session-start-form.tsx +61 -12
  188. package/blocks/dialer/components/softphone.tsx +4 -2
  189. package/blocks/enrichment/README.md +29 -7
  190. package/blocks/enrichment/block.json +49 -9
  191. package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
  192. package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
  193. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  194. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  195. package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
  196. package/blocks/enrichment/components/waterfall-config.tsx +66 -17
  197. package/blocks/enrichment/lib/enrichment-types.ts +39 -0
  198. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  199. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  200. package/blocks/forms/README.md +8 -0
  201. package/blocks/forms/block.json +139 -18
  202. package/blocks/forms/components/form-activity.tsx +71 -0
  203. package/blocks/forms/components/form-backfill.tsx +108 -0
  204. package/blocks/forms/components/form-builder.tsx +4 -2
  205. package/blocks/forms/components/form-embed.tsx +4 -2
  206. package/blocks/forms/components/form-groups.tsx +151 -0
  207. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  208. package/blocks/forms/components/form-route-history.tsx +72 -0
  209. package/blocks/forms/components/form-saved-views.tsx +81 -0
  210. package/blocks/forms/components/forms-screen.tsx +56 -19
  211. package/blocks/forms/components/more-rows.tsx +30 -0
  212. package/blocks/forms/components/submissions-export.tsx +67 -0
  213. package/blocks/forms/components/submissions-table.tsx +17 -6
  214. package/blocks/forms/lib/forms-extra.ts +67 -0
  215. package/blocks/inbox/README.md +32 -3
  216. package/blocks/inbox/block.json +29 -2
  217. package/blocks/inbox/components/inbox-list.tsx +7 -3
  218. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  219. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  220. package/blocks/inbox/components/reply-composer.tsx +65 -9
  221. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  222. package/blocks/inbox/components/thread-badge.tsx +2 -2
  223. package/blocks/inbox/components/thread-view.tsx +1 -1
  224. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  225. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  226. package/blocks/inbox/lib/inbox-types.ts +47 -1
  227. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  228. package/blocks/integrations/README.md +63 -1
  229. package/blocks/integrations/block.json +234 -15
  230. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  231. package/blocks/integrations/components/connection-health.tsx +59 -0
  232. package/blocks/integrations/components/crm-detail.tsx +119 -0
  233. package/blocks/integrations/components/crm-records.tsx +103 -0
  234. package/blocks/integrations/components/field-mappings.tsx +61 -0
  235. package/blocks/integrations/components/integration-list.tsx +13 -8
  236. package/blocks/integrations/components/integrations-screen.tsx +152 -0
  237. package/blocks/integrations/components/migration-panel.tsx +133 -0
  238. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  239. package/blocks/integrations/components/sync-status.tsx +11 -9
  240. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  241. package/blocks/integrations/components/webhook-detail.tsx +179 -0
  242. package/blocks/integrations/components/webhook-manager.tsx +43 -25
  243. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  244. package/blocks/integrations/lib/crm-utils.ts +39 -0
  245. package/blocks/integrations/lib/facts.tsx +13 -0
  246. package/blocks/integrations/lib/integrations-utils.ts +11 -1
  247. package/blocks/integrations/lib/load-more.tsx +141 -0
  248. package/blocks/intent/README.md +14 -0
  249. package/blocks/intent/block.json +27 -1
  250. package/blocks/intent/components/account-intent-score.tsx +5 -5
  251. package/blocks/intent/components/intent-feed.tsx +13 -10
  252. package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
  253. package/blocks/intent/components/intent-screen.tsx +265 -0
  254. package/blocks/intent/components/keyword-detail.tsx +124 -0
  255. package/blocks/intent/lib/intent-utils.ts +4 -4
  256. package/blocks/lifecycle/block.json +13 -8
  257. package/blocks/lifecycle/components/account-related.tsx +8 -1
  258. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  259. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  260. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  261. package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
  262. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  263. package/blocks/lists/README.md +20 -1
  264. package/blocks/lists/block.json +69 -3
  265. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  266. package/blocks/lists/components/add-to-list-button.tsx +20 -9
  267. package/blocks/lists/components/list-picker.tsx +17 -3
  268. package/blocks/lists/components/list-table.tsx +12 -6
  269. package/blocks/lists/components/lists-screen.tsx +346 -0
  270. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  271. package/blocks/lists/components/suppression-manager.tsx +33 -8
  272. package/blocks/lists/components/suppressions-screen.tsx +264 -0
  273. package/blocks/mailboxes/README.md +17 -0
  274. package/blocks/mailboxes/block.json +69 -17
  275. package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
  276. package/blocks/mailboxes/components/domain-detail.tsx +148 -0
  277. package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
  278. package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
  279. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  280. package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
  281. package/blocks/mailboxes/components/warmup-health.tsx +24 -22
  282. package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
  283. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  284. package/blocks/newsletter/README.md +12 -0
  285. package/blocks/newsletter/block.json +157 -24
  286. package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
  287. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  288. package/blocks/newsletter/components/more-rows.tsx +30 -0
  289. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  290. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  291. package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
  292. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  293. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  294. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  295. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  296. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  297. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  298. package/blocks/nurture/block.json +28 -14
  299. package/blocks/nurture/components/nurture-builder.tsx +13 -3
  300. package/blocks/nurture/components/nurture-enroll.tsx +12 -10
  301. package/blocks/nurture/components/nurture-form.tsx +27 -10
  302. package/blocks/nurture/components/nurture-screen.tsx +26 -11
  303. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  304. package/blocks/nurture/lib/nurture-types.ts +1 -1
  305. package/blocks/objects/README.md +18 -1
  306. package/blocks/objects/block.json +42 -21
  307. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  308. package/blocks/objects/components/object-not-found.tsx +3 -1
  309. package/blocks/objects/components/object-record-detail.tsx +10 -2
  310. package/blocks/objects/components/object-schema-editor.tsx +4 -2
  311. package/blocks/objects/components/object-table.tsx +13 -3
  312. package/blocks/objects/components/objects-screen.tsx +147 -0
  313. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  314. package/blocks/objects/lib/object-types.ts +1 -1
  315. package/blocks/pages/README.md +14 -0
  316. package/blocks/pages/block.json +140 -23
  317. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  318. package/blocks/pages/components/content-library.tsx +9 -4
  319. package/blocks/pages/components/more-rows.tsx +30 -0
  320. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  321. package/blocks/pages/components/page-analytics.tsx +176 -0
  322. package/blocks/pages/components/page-clone.tsx +99 -0
  323. package/blocks/pages/components/page-domains.tsx +167 -0
  324. package/blocks/pages/components/page-editor.tsx +1 -1
  325. package/blocks/pages/components/page-generation-status.tsx +48 -0
  326. package/blocks/pages/components/page-list.tsx +2 -1
  327. package/blocks/pages/components/page-tracking.tsx +137 -0
  328. package/blocks/pages/components/pages-screen.tsx +57 -16
  329. package/blocks/pages/components/seo-report.tsx +6 -1
  330. package/blocks/pages/lib/studio.ts +7 -0
  331. package/blocks/phone-numbers/README.md +74 -0
  332. package/blocks/phone-numbers/block.json +262 -0
  333. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  334. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  335. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  336. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  337. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  338. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  339. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  340. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  341. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  342. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  343. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  344. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  345. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  346. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  347. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  348. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  349. package/blocks/pickers/README.md +23 -0
  350. package/blocks/pickers/block.json +103 -0
  351. package/blocks/pickers/components/company-picker.tsx +90 -0
  352. package/blocks/pickers/components/contact-picker.tsx +131 -0
  353. package/blocks/pickers/components/member-picker.tsx +99 -0
  354. package/blocks/pickers/components/search-picker.tsx +98 -0
  355. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  356. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  357. package/blocks/pickers/lib/use-debounced.ts +13 -0
  358. package/blocks/quotes/README.md +13 -0
  359. package/blocks/quotes/block.json +65 -13
  360. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  361. package/blocks/quotes/components/product-picker.tsx +3 -1
  362. package/blocks/quotes/components/quote-builder.tsx +1 -1
  363. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  364. package/blocks/quotes/components/quote-list.tsx +5 -3
  365. package/blocks/quotes/components/quote-related.tsx +8 -4
  366. package/blocks/quotes/components/quote-viewer.tsx +7 -2
  367. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  368. package/blocks/radar/README.md +10 -0
  369. package/blocks/radar/block.json +41 -1
  370. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  371. package/blocks/radar/components/competitor-detail.tsx +117 -0
  372. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  373. package/blocks/radar/components/radar-overview.tsx +4 -4
  374. package/blocks/radar/components/radar-report.tsx +4 -2
  375. package/blocks/radar/components/radar-screen.tsx +214 -0
  376. package/blocks/radar/components/report-detail.tsx +104 -0
  377. package/blocks/search/README.md +10 -0
  378. package/blocks/search/block.json +16 -1
  379. package/blocks/search/components/filter-builder.tsx +3 -3
  380. package/blocks/search/components/prospect-results.tsx +5 -3
  381. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  382. package/blocks/search/components/saved-searches.tsx +4 -1
  383. package/blocks/search/hooks/use-filter-options.ts +20 -6
  384. package/blocks/search/lib/search-types.ts +2 -0
  385. package/blocks/sequencer/README.md +55 -9
  386. package/blocks/sequencer/block.json +126 -19
  387. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  388. package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
  389. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  390. package/blocks/sequencer/components/sequence-builder.tsx +224 -60
  391. package/blocks/sequencer/components/sequence-detail.tsx +261 -0
  392. package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
  393. package/blocks/sequencer/components/sequence-list.tsx +34 -9
  394. package/blocks/sequencer/components/sequence-stats.tsx +11 -10
  395. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  396. package/blocks/sequencer/components/sequences-screen.tsx +162 -0
  397. package/blocks/sequencer/components/step-editor.tsx +14 -12
  398. package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
  399. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  400. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  401. package/blocks/social-ads/README.md +19 -0
  402. package/blocks/social-ads/block.json +233 -1
  403. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  404. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  405. package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
  406. package/blocks/social-ads/components/audience-sync.tsx +17 -9
  407. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  408. package/blocks/social-ads/components/campaign-detail.tsx +138 -0
  409. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  410. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  411. package/blocks/social-ads/components/creative-detail.tsx +155 -0
  412. package/blocks/social-ads/components/creative-gallery.tsx +35 -7
  413. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  414. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  415. package/blocks/social-ads/components/post-composer.tsx +7 -3
  416. package/blocks/social-ads/components/post-detail.tsx +163 -0
  417. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  418. package/blocks/social-ads/components/social-performance.tsx +80 -0
  419. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  420. package/blocks/social-ads/components/social-screen.tsx +187 -0
  421. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  422. package/blocks/surveys/README.md +91 -0
  423. package/blocks/surveys/block.json +299 -0
  424. package/blocks/surveys/components/account-nps.tsx +69 -0
  425. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  426. package/blocks/surveys/components/org-summary.tsx +82 -0
  427. package/blocks/surveys/components/sending-domains.tsx +113 -0
  428. package/blocks/surveys/components/survey-form.tsx +126 -0
  429. package/blocks/surveys/components/survey-responses.tsx +145 -0
  430. package/blocks/surveys/components/survey-results.tsx +72 -0
  431. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  432. package/blocks/surveys/components/survey-send.tsx +162 -0
  433. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  434. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  435. package/blocks/surveys/lib/surveys-types.ts +228 -0
  436. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  437. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  438. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  439. package/blocks/visitors/README.md +10 -0
  440. package/blocks/visitors/block.json +25 -1
  441. package/blocks/visitors/components/snippet-install.tsx +21 -10
  442. package/blocks/visitors/components/traffic-chart.tsx +4 -4
  443. package/blocks/visitors/components/visitor-company-card.tsx +26 -16
  444. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  445. package/blocks/visitors/components/visitor-feed.tsx +16 -7
  446. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  447. package/blocks/webchat/README.md +10 -0
  448. package/blocks/webchat/block.json +21 -7
  449. package/blocks/webchat/components/chat-widget-config.tsx +8 -5
  450. package/blocks/webchat/components/conversation-list.tsx +8 -4
  451. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  452. package/blocks/workbench/README.md +11 -0
  453. package/blocks/workbench/block.json +48 -1
  454. package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
  455. package/blocks/workbench/components/run-progress.tsx +1 -1
  456. package/blocks/workbench/components/workbench-grid.tsx +10 -4
  457. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  458. package/blocks/workbench/components/workbench-screen.tsx +202 -0
  459. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  460. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  461. package/blocks/workflows/README.md +13 -0
  462. package/blocks/workflows/block.json +28 -9
  463. package/blocks/workflows/components/run-history.tsx +31 -9
  464. package/blocks/workflows/components/trigger-config.tsx +3 -1
  465. package/blocks/workflows/components/workflow-editor.tsx +4 -2
  466. package/blocks/workflows/components/workflow-list.tsx +13 -6
  467. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  468. package/dist/cli.js +3109 -1977
  469. package/dist/errors.d.mts +34 -0
  470. package/dist/errors.d.ts +34 -0
  471. package/dist/errors.js +46 -0
  472. package/dist/errors.js.map +1 -0
  473. package/dist/errors.mjs +21 -0
  474. package/dist/errors.mjs.map +1 -0
  475. package/dist/index.browser.js +11 -5
  476. package/dist/index.browser.js.map +1 -1
  477. package/dist/index.browser.mjs +11 -5
  478. package/dist/index.browser.mjs.map +1 -1
  479. package/dist/index.d.mts +164 -25
  480. package/dist/index.d.ts +164 -25
  481. package/dist/index.js +11 -5
  482. package/dist/index.js.map +1 -1
  483. package/dist/index.mjs +11 -5
  484. package/dist/index.mjs.map +1 -1
  485. package/dist/react.browser.js +10 -4
  486. package/dist/react.browser.js.map +1 -1
  487. package/dist/react.browser.mjs +10 -4
  488. package/dist/react.browser.mjs.map +1 -1
  489. package/dist/react.d.mts +142 -5
  490. package/dist/react.d.ts +142 -5
  491. package/dist/react.js +10 -4
  492. package/dist/react.js.map +1 -1
  493. package/dist/react.mjs +10 -4
  494. package/dist/react.mjs.map +1 -1
  495. package/dist/types/blocks/add.d.ts +11 -4
  496. package/dist/types/blocks/brand.d.ts +122 -0
  497. package/dist/types/blocks/compose.d.ts +53 -0
  498. package/dist/types/blocks/create.d.ts +27 -20
  499. package/dist/types/blocks/doctor.d.ts +6 -1
  500. package/dist/types/blocks/flows.d.ts +7 -1
  501. package/dist/types/blocks/navfile.d.ts +35 -0
  502. package/dist/types/blocks/orgBrand.d.ts +34 -0
  503. package/dist/types/blocks/paths.d.ts +4 -0
  504. package/dist/types/blocks/prefixes.d.ts +37 -0
  505. package/dist/types/blocks/registry.d.ts +17 -0
  506. package/dist/types/blocks/screens.d.ts +53 -0
  507. package/dist/types/cli.d.ts +5 -0
  508. package/dist/types/errors.d.ts +33 -0
  509. package/dist/types/http.d.ts +2 -18
  510. package/dist/types/version.d.ts +2 -2
  511. package/docs/agents.md +15 -4
  512. package/docs/api/README.md +2 -2
  513. package/docs/api/by-path.md +1 -0
  514. package/docs/api/desk.d.ts +98 -2
  515. package/docs/api/desk.md +123 -1
  516. package/docs/api/inbox.d.ts +10 -2
  517. package/docs/api/inbox.md +10 -2
  518. package/docs/api/seo.d.ts +19 -1
  519. package/docs/api/seo.md +24 -1
  520. package/package.json +16 -2
  521. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  522. package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
  523. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  524. package/stacks/6sense.json +55 -17
  525. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  526. package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
  527. package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
  528. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  529. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  530. package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
  531. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  532. package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
  533. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  534. package/stacks/apollo.json +147 -37
  535. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  536. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  537. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  538. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  539. package/stacks/calendly.json +64 -0
  540. package/stacks/clay/pages/companies.tsx.tmpl +31 -0
  541. package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
  542. package/stacks/clay/pages/lists.tsx.tmpl +12 -0
  543. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  544. package/stacks/clay.json +73 -27
  545. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  546. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  547. package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
  548. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  549. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  550. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  551. package/stacks/gong.json +98 -35
  552. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  553. package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
  554. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  555. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  556. package/stacks/hootsuite.json +104 -0
  557. package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
  558. package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
  559. package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
  560. package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
  561. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  562. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  563. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  564. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  565. package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
  566. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  567. package/stacks/hubspot.json +181 -64
  568. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  569. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  570. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  571. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  572. package/stacks/intercom.json +52 -20
  573. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  574. package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
  575. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  576. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  577. package/stacks/mailchimp.json +68 -34
  578. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  579. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  580. package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
  581. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  582. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  583. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  584. package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
  585. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  586. package/stacks/outreach.json +106 -25
  587. package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
  588. package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
  589. package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
  590. package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
  591. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  592. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  593. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  594. package/stacks/pipedrive.json +117 -0
  595. package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
  596. package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
  597. package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
  598. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  599. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  600. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  601. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  602. package/stacks/salesforce.json +85 -31
  603. package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
  604. package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
  605. package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
  606. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  607. package/stacks/zoominfo.json +115 -0
  608. package/styles/styles.css +1 -1
  609. package/templates/next-app/README.md.tmpl +40 -9
  610. package/templates/next-app/app/globals.css.tmpl +349 -64
  611. package/templates/next-app/app/layout.tsx.tmpl +7 -2
  612. package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
  613. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
  614. package/templates/next-app/dot-gitignore.tmpl +2 -0
  615. package/templates/next-app/package.json.tmpl +1 -1
@@ -0,0 +1,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
+ }
@@ -98,3 +98,9 @@ with the full API key.
98
98
  - `03-bookings-digest.ts`: upcoming bookings, pending approvals, recorded meetings.
99
99
  - `04-calendar-invite.ts`: ad-hoc invite via `calendarInvites.createCalendarInvite` (not part of the block's
100
100
  UI; add the operation to your own proxy allowlist if you call it from the browser).
101
+
102
+ ## Cross-block glue
103
+
104
+ - `MeetingContactLinks` (`components/meeting-contact-links.tsx`): "Open <attendee>" buttons for a booked or recorded meeting, one per attendee who is a CRM contact. Use it in `MeetingsScreen`'s `renderDetailActions`.
105
+
106
+ - `RecordMeetingsTab` (`components/record-meetings-tab.tsx`): meetings booked with a contact (`getContactMeetings`, the API's 50 most recent booking events), each opening `/meetings?sel=booking:<id>`. `extraDetailTabs` entry for ContactsScreen.
@@ -23,10 +23,12 @@
23
23
  "get_availability_appointments_schedules__schedule_id__availability_get",
24
24
  "get_available_slots_appointments_slots_get",
25
25
  "get_booking_appointments_bookings__booking_uid__get",
26
+ "get_contact_meetings_contacts__contact_id__meetings_get",
26
27
  "get_event_type_appointments_event_types__event_type_id__get",
27
28
  "get_meeting_inbox_meetings__meeting_id__get",
28
29
  "list_bookings_appointments_bookings_get",
29
30
  "list_calendars_appointments_calendars_get",
31
+ "list_contacts_contacts_get",
30
32
  "list_event_types_event_types_get",
31
33
  "list_meetings_inbox_meetings_get",
32
34
  "list_schedules_appointments_schedules_get",
@@ -113,8 +115,12 @@
113
115
  "name": "MeetingsScreen",
114
116
  "file": "components/meetings-screen.tsx",
115
117
  "purpose": "Finished meetings product: upcoming, past, cancelled and recorded meetings with detail, approvals, notes and a schedule-a-meeting flow.",
116
- "operations": [],
117
- "tiers": []
118
+ "operations": [
119
+ "list_bookings_appointments_bookings_get"
120
+ ],
121
+ "tiers": [
122
+ "read"
123
+ ]
118
124
  },
119
125
  {
120
126
  "name": "SchedulingScreen",
@@ -134,6 +140,30 @@
134
140
  "tiers": [
135
141
  "read"
136
142
  ]
143
+ },
144
+ {
145
+ "name": "MeetingContactLinks",
146
+ "file": "components/meeting-contact-links.tsx",
147
+ "purpose": "\"Open <attendee>\" buttons for a booked or recorded meeting: one per attendee who is a CRM contact; for MeetingsScreen renderDetailActions.",
148
+ "operations": [
149
+ "get_booking_appointments_bookings__booking_uid__get",
150
+ "get_meeting_inbox_meetings__meeting_id__get",
151
+ "list_contacts_contacts_get"
152
+ ],
153
+ "tiers": [
154
+ "read"
155
+ ]
156
+ },
157
+ {
158
+ "name": "RecordMeetingsTab",
159
+ "file": "components/record-meetings-tab.tsx",
160
+ "purpose": "The meetings booked with one contact (booked, rescheduled, cancelled), each opening in the meetings screen; an extraDetailTabs entry for ContactsScreen.",
161
+ "operations": [
162
+ "get_contact_meetings_contacts__contact_id__meetings_get"
163
+ ],
164
+ "tiers": [
165
+ "read"
166
+ ]
137
167
  }
138
168
  ],
139
169
  "hooks": [
@@ -216,6 +246,7 @@
216
246
  ],
217
247
  "scopes": [
218
248
  "campaigns:read",
249
+ "contacts:read",
219
250
  "meetings:delete",
220
251
  "meetings:read",
221
252
  "meetings:run",
@@ -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
  );
@@ -98,7 +98,7 @@ export function BookingPage({ eventTypeId, daysAhead = 14, timeZone, defaultAtte
98
98
  if (book.data) {
99
99
  return (
100
100
  <div className={`space-y-2 rounded-lg border p-4 ${className ?? ""}`} role="status">
101
- <h2 className="text-base font-semibold">You are booked</h2>
101
+ <h2 className="text-base font-medium">You are booked</h2>
102
102
  <p className="text-sm">
103
103
  {eventType.data?.title ?? "Meeting"} on {formatDateTime(bookedAt ?? slot?.time, zone)}. A calendar invite was sent to {email}.
104
104
  </p>
@@ -116,7 +116,7 @@ export function BookingPage({ eventTypeId, daysAhead = 14, timeZone, defaultAtte
116
116
  return (
117
117
  <div className={`space-y-4 ${className ?? ""}`}>
118
118
  <header>
119
- <h2 className="text-base font-semibold">{eventType.data?.title ?? "Book a meeting"}</h2>
119
+ <h2 className="text-base font-medium">{eventType.data?.title ?? "Book a meeting"}</h2>
120
120
  <p className="text-sm text-muted-foreground">
121
121
  {eventType.data ? `${eventType.data.length} minutes · ` : ""}times shown in {zone}
122
122
  </p>
@@ -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>
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ /**
4
+ * MeetingContactLinks: "Open <attendee>" buttons for a booked or recorded meeting, one per attendee (at most three)
5
+ * who is a contact in your CRM. Use it in MeetingsScreen's `renderDetailActions`:
6
+ * renderDetailActions={(selection) => <MeetingContactLinks selection={selection} onOpenContact={(id) => router.push(`/contacts?id=${id}`)} />}
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. getBooking GET /appointments/bookings/{uid} (read) for a booking
10
+ * getMeeting GET /inbox/meetings/{id} (read) for a recorded meeting
11
+ * 2. listContacts GET /contacts?email= (read) once per attendee, via the crm ContactLookupLink
12
+ * Nothing here is billable, external or destructive.
13
+ */
14
+
15
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
16
+ import { g8Get } from "@/g8/lib/g8-client";
17
+ import { ContactLookupLink } from "@/g8/crm/components/contact-lookup-link";
18
+ import { unwrap } from "../lib/appointments-types";
19
+ import type { MeetingSelection } from "./meeting-list";
20
+
21
+ interface Attendee { email?: string | null; name?: string | null }
22
+
23
+ export interface MeetingContactLinksProps {
24
+ selection: MeetingSelection;
25
+ /** Route to the contact in your contacts screen. */
26
+ onOpenContact: (contactId: number) => void;
27
+ }
28
+
29
+ export function MeetingContactLinks({ selection, onOpenContact }: MeetingContactLinksProps) {
30
+ const { kind, id } = selection;
31
+ const q = useG8Query(async (signal) => {
32
+ const path = kind === "booking" ? `/appointments/bookings/${encodeURIComponent(id)}` : `/inbox/meetings/${encodeURIComponent(id)}`;
33
+ return unwrap<{ attendees?: Attendee[] | null }>(await g8Get(path, undefined, { signal })).attendees ?? [];
34
+ }, [kind, id]);
35
+ const seen = new Set<string>();
36
+ const people = (q.data ?? []).filter((a) => {
37
+ const email = a.email?.trim().toLowerCase();
38
+ if (!email || seen.has(email)) return false;
39
+ seen.add(email);
40
+ return true;
41
+ }).slice(0, 3);
42
+ return (
43
+ <>
44
+ {people.map((a) => (
45
+ <ContactLookupLink key={a.email} email={a.email} label={`Open ${a.name || a.email}`} onOpen={onOpenContact} />
46
+ ))}
47
+ </>
48
+ );
49
+ }
@@ -48,7 +48,7 @@ function RecordedDetail({ id, className }: { id: string; className?: string }) {
48
48
  const m = q.data;
49
49
  return (
50
50
  <section className={`space-y-3 ${className ?? ""}`}>
51
- <h2 className="text-base font-semibold">{m.subject || "Untitled meeting"}</h2>
51
+ <h2 className="text-base font-medium">{m.subject || "Untitled meeting"}</h2>
52
52
  <dl className="grid gap-3 sm:grid-cols-2">
53
53
  <Field label="When">{formatDateTime(m.start_time)}</Field>
54
54
  <Field label="Attendees">{(m.attendees ?? []).map((a) => a.name || a.email).join(", ") || "Unknown"}</Field>
@@ -94,7 +94,7 @@ function BookingDetail({ uid, onChanged, className }: { uid: string; onChanged?:
94
94
 
95
95
  return (
96
96
  <section className={`space-y-4 ${className ?? ""}`}>
97
- <h2 className="text-base font-semibold">{b.title || b.event_type_title || "Meeting"}</h2>
97
+ <h2 className="text-base font-medium">{b.title || b.event_type_title || "Meeting"}</h2>
98
98
  <dl className="grid gap-3 sm:grid-cols-2">
99
99
  <Field label="When">{formatDateTime(b.start_time)}</Field>
100
100
  <Field label="Status"><span className="capitalize">{b.status}</span></Field>
@@ -18,12 +18,18 @@ import { ApiError } from "@/g8/components/api-error";
18
18
  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
+ import { useUrlFilters } from "@/g8/hooks/use-url-filters";
22
+ import { SelectFilter, TextFilter } from "@/g8/components/screen/filter-controls";
21
23
  import { formatDateTime, listOf, type Booking, type RecordedMeeting } from "../lib/appointments-types";
24
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
25
+ import { RecordLink } from "@/g8/components/screen/record-link";
22
26
 
23
27
  export type MeetingView = "upcoming" | "past" | "cancelled" | "recorded";
24
28
  export type MeetingSelection = { kind: "booking"; id: string } | { kind: "meeting"; id: string };
25
29
 
26
30
  export interface MeetingListProps {
31
+ /** 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. */
32
+ linkParam?: string;
27
33
  onSelect?: (selection: MeetingSelection) => void;
28
34
  initialView?: MeetingView;
29
35
  pageSize?: number;
@@ -41,8 +47,8 @@ const VIEWS: ReadonlyArray<{ value: MeetingView; label: string }> = [
41
47
  { value: "recorded", label: "Recorded" },
42
48
  ];
43
49
 
44
- function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number) {
45
- const base = { skip: page * take, take };
50
+ function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number, attendeeEmail?: string) {
51
+ const base = { skip: page * take, take, attendee_email: attendeeEmail || undefined };
46
52
  if (view === "upcoming") return { ...base, after_start: now, sort_by: "start_time", sort_order: "asc" };
47
53
  if (view === "past") return { ...base, before_end: now, sort_by: "start_time", sort_order: "desc" };
48
54
  return { ...base, status: "cancelled", sort_by: "start_time", sort_order: "desc" };
@@ -52,36 +58,38 @@ function attendeeNames(b: Booking): string {
52
58
  return b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees";
53
59
  }
54
60
 
55
- function useDebouncedText(value: string, ms = 300): string {
56
- const [v, setV] = useState(value);
57
- useEffect(() => { const t = setTimeout(() => setV(value.trim()), ms); return () => clearTimeout(t); }, [value, ms]);
58
- return v;
59
- }
61
+ /** Every filter the meetings (/inbox/meetings) and bookings APIs take besides the view tab. */
62
+ const MEETING_FILTERS = ["search", "participant_email", "timeframe", "has_transcript", "attendee_email"] as const;
60
63
 
61
- export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
64
+ export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
62
65
  const [view, setView] = useState<MeetingView>(initialView);
63
66
  const [page, setPage] = useState(0);
64
- const [search, setSearch] = useState("");
65
- const query = useDebouncedText(search);
67
+ // Every filter the meetings and bookings APIs take lives in the URL (`m_participant_email=...`).
68
+ const uf = useUrlFilters(MEETING_FILTERS, { prefix: "m_" });
69
+ const search = uf.draft.search;
70
+ const query = uf.filters.search.trim();
71
+ const { participant_email, timeframe, has_transcript, attendee_email } = uf.filters;
66
72
  const now = useMemo(() => new Date().toISOString(), [view]); // eslint-disable-line react-hooks/exhaustive-deps
67
73
 
68
74
  const rows = useG8Query(
69
75
  async (signal) => {
70
76
  if (view === "recorded") {
71
- const env = await g8Get("/inbox/meetings", { search: query || undefined, page: page + 1, page_size: pageSize }, { signal });
77
+ const env = await g8Get("/inbox/meetings", { search: query || undefined, participant_email: participant_email ? participant_email.split(",").map((e) => e.trim()).filter(Boolean) : undefined, timeframe: timeframe || undefined, has_transcript: has_transcript ? has_transcript === "yes" : undefined, page: page + 1, page_size: pageSize }, { signal });
72
78
  return { kind: "meeting" as const, items: listOf<RecordedMeeting>(env), more: Boolean((env as { pagination?: { has_next?: boolean } }).pagination?.has_next) };
73
79
  }
74
- const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize), { signal });
80
+ const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize, attendee_email.trim()), { signal });
75
81
  const items = listOf<Booking>(env, "bookings");
76
82
  return { kind: "booking" as const, items, more: items.length === pageSize };
77
83
  },
78
- [view, page, query, pageSize, now, refreshKey],
84
+ [view, page, query, participant_email, timeframe, has_transcript, attendee_email, pageSize, now, refreshKey],
85
+ { enabled: uf.ready },
79
86
  );
80
87
 
81
88
  const switchView = (next: MeetingView) => {
82
89
  setView(next);
83
90
  setPage(0);
84
91
  };
92
+ useEffect(() => { setPage(0); }, [query, participant_email, timeframe, has_transcript, attendee_email]);
85
93
  const result = rows.data;
86
94
 
87
95
  return (
@@ -94,7 +102,19 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
94
102
  </Button>
95
103
  ))}
96
104
  </div>
97
- {view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} /> : null}
105
+ {view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) => uf.set("search", e.target.value)} /> : null}
106
+ </div>
107
+ <div className="flex flex-wrap items-center gap-2">
108
+ {view === "recorded" ? (
109
+ <>
110
+ <TextFilter label="participant email" placeholder="Participant email" type="email" value={uf.draft.participant_email} onChange={(v) => uf.set("participant_email", v)} className="w-52" />
111
+ <SelectFilter label="timeframe" any="Past (default)" value={uf.draft.timeframe} onChange={(v) => uf.set("timeframe", v)} options={[{ value: "upcoming", label: "Upcoming" }, { value: "all", label: "All" }]} />
112
+ <SelectFilter label="transcript" any="With or without transcript" value={uf.draft.has_transcript} onChange={(v) => uf.set("has_transcript", v)} options={[{ value: "yes", label: "Has a transcript" }, { value: "no", label: "No transcript" }]} />
113
+ </>
114
+ ) : (
115
+ <TextFilter label="attendee email" placeholder="Attendee email" type="email" value={uf.draft.attendee_email} onChange={(v) => uf.set("attendee_email", v)} className="w-52" />
116
+ )}
117
+ {uf.active ? <Button size="sm" variant="ghost" onClick={uf.clear}>Clear filters</Button> : null}
98
118
  </div>
99
119
 
100
120
  {rows.error ? (
@@ -120,17 +140,17 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
120
140
  <TableBody>
121
141
  {result.kind === "booking"
122
142
  ? (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 })}>
143
+ <TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "booking", id: b.uid })) : {})}>
124
144
  <TableCell className="whitespace-nowrap">{formatDateTime(b.start_time)}</TableCell>
125
- <TableCell className="font-medium">{b.title || b.event_type_title || "Meeting"}</TableCell>
145
+ <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
146
  <TableCell className="max-w-64 truncate">{attendeeNames(b)}</TableCell>
127
147
  <TableCell className="capitalize">{b.status}</TableCell>
128
148
  </TableRow>
129
149
  ))
130
150
  : (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 })}>
151
+ <TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "meeting", id: m.id })) : {})}>
132
152
  <TableCell className="whitespace-nowrap">{formatDateTime(m.start_time)}</TableCell>
133
- <TableCell className="font-medium">{m.subject || "Untitled meeting"}</TableCell>
153
+ <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
154
  <TableCell className="max-w-64 truncate">{m.transcript_summary || "No summary yet"}</TableCell>
135
155
  <TableCell className="capitalize">{m.transcript_status ?? "none"}</TableCell>
136
156
  </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>