@graph8/sdk 0.361.0 → 0.370.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +14 -10
  2. package/blocks/_foundation/README.md +172 -1
  3. package/blocks/_foundation/block.json +234 -13
  4. package/blocks/_foundation/components/api-error.tsx +71 -25
  5. package/blocks/_foundation/components/brand-provider.tsx +23 -0
  6. package/blocks/_foundation/components/confirmed-action.tsx +90 -0
  7. package/blocks/_foundation/components/connect-graph8.tsx +280 -0
  8. package/blocks/_foundation/components/empty-state.tsx +39 -7
  9. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  10. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  11. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  12. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  13. package/blocks/_foundation/components/screen/index.ts +22 -0
  14. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  15. package/blocks/_foundation/components/screen/record-link.tsx +76 -0
  16. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  17. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  18. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  19. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  20. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  21. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  22. package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
  23. package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
  24. package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
  25. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  26. package/blocks/_foundation/hooks/use-brand.ts +52 -0
  27. package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
  28. package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
  29. package/blocks/_foundation/hooks/use-paginated.ts +120 -0
  30. package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
  31. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  32. package/blocks/_foundation/lib/g8-cache.ts +454 -0
  33. package/blocks/_foundation/lib/g8-client.ts +1 -1
  34. package/blocks/_foundation/lib/plural.ts +23 -0
  35. package/blocks/_foundation/server/connect-route.ts +15 -0
  36. package/blocks/_foundation/server/connect.ts +127 -0
  37. package/blocks/_foundation/server/local-key.ts +70 -0
  38. package/blocks/_foundation/server/proxy.ts +103 -3
  39. package/blocks/_foundation/server/route.ts +16 -4
  40. package/blocks/agents/README.md +14 -0
  41. package/blocks/agents/block.json +33 -1
  42. package/blocks/agents/components/agent-builder.tsx +13 -8
  43. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
  44. package/blocks/agents/components/agent-run-detail.tsx +102 -0
  45. package/blocks/agents/components/agent-runs.tsx +16 -7
  46. package/blocks/agents/components/agents-screen.tsx +184 -0
  47. package/blocks/analytics/README.md +13 -0
  48. package/blocks/analytics/block.json +45 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +209 -0
  50. package/blocks/analytics/components/leaderboard.tsx +14 -7
  51. package/blocks/analytics/components/report-builder.tsx +23 -9
  52. package/blocks/analytics/components/report-detail.tsx +84 -0
  53. package/blocks/analytics/components/trend-chart.tsx +62 -0
  54. package/blocks/appointments/README.md +21 -1
  55. package/blocks/appointments/block.json +43 -2
  56. package/blocks/appointments/components/availability-editor.tsx +17 -5
  57. package/blocks/appointments/components/event-type-editor.tsx +2 -1
  58. package/blocks/appointments/components/event-type-related.tsx +77 -0
  59. package/blocks/appointments/components/meeting-list.tsx +14 -6
  60. package/blocks/appointments/components/meetings-screen.tsx +157 -0
  61. package/blocks/appointments/components/scheduling-screen.tsx +122 -0
  62. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  63. package/blocks/block.schema.json +18 -0
  64. package/blocks/block.types.ts +26 -0
  65. package/blocks/campaigns/README.md +41 -11
  66. package/blocks/campaigns/block.json +105 -42
  67. package/blocks/campaigns/components/campaign-assets.tsx +16 -5
  68. package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
  69. package/blocks/campaigns/components/campaign-list.tsx +7 -3
  70. package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
  71. package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
  72. package/blocks/campaigns/components/icp-detail.tsx +154 -0
  73. package/blocks/campaigns/components/icp-editor.tsx +15 -15
  74. package/blocks/campaigns/components/persona-detail.tsx +172 -0
  75. package/blocks/campaigns/components/persona-editor.tsx +14 -11
  76. package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
  77. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
  78. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
  79. package/blocks/coach/README.md +10 -0
  80. package/blocks/coach/block.json +16 -1
  81. package/blocks/coach/components/coach-screen.tsx +222 -0
  82. package/blocks/copilot/README.md +13 -0
  83. package/blocks/copilot/block.json +14 -1
  84. package/blocks/copilot/components/copilot-panel.tsx +14 -5
  85. package/blocks/copilot/components/copilot-screen.tsx +148 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
  87. package/blocks/crm/README.md +28 -0
  88. package/blocks/crm/block.json +58 -2
  89. package/blocks/crm/components/companies-screen.tsx +345 -0
  90. package/blocks/crm/components/company-detail.tsx +8 -6
  91. package/blocks/crm/components/company-table.tsx +11 -4
  92. package/blocks/crm/components/contact-detail.tsx +6 -3
  93. package/blocks/crm/components/contact-table.tsx +10 -3
  94. package/blocks/crm/components/contacts-screen.tsx +359 -0
  95. package/blocks/crm/components/dedupe-review.tsx +7 -5
  96. package/blocks/crm/components/import-wizard.tsx +4 -2
  97. package/blocks/crm/components/notes-panel.tsx +4 -4
  98. package/blocks/crm/components/record-form.tsx +5 -3
  99. package/blocks/crm/components/task-list.tsx +4 -4
  100. package/blocks/crm/components/tasks-screen.tsx +379 -0
  101. package/blocks/crm/hooks/use-custom-fields.ts +6 -1
  102. package/blocks/crm/hooks/use-page-query.ts +7 -1
  103. package/blocks/crm/lib/crm-types.ts +12 -2
  104. package/blocks/crm/lib/screen-parts.tsx +105 -0
  105. package/blocks/deals/README.md +34 -3
  106. package/blocks/deals/block.json +45 -4
  107. package/blocks/deals/components/deal-detail.tsx +27 -13
  108. package/blocks/deals/components/deal-related.tsx +87 -0
  109. package/blocks/deals/components/deals-screen.tsx +298 -0
  110. package/blocks/deals/components/pipeline-board.tsx +4 -2
  111. package/blocks/deals/components/stage-editor.tsx +4 -4
  112. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  113. package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
  114. package/blocks/deals/hooks/use-pipelines.ts +1 -1
  115. package/blocks/deals/lib/deals-types.ts +14 -0
  116. package/blocks/dialer/README.md +34 -2
  117. package/blocks/dialer/block.json +67 -1
  118. package/blocks/dialer/components/call-contact-button.tsx +252 -0
  119. package/blocks/dialer/components/call-log-screen.tsx +205 -0
  120. package/blocks/dialer/components/call-log.tsx +7 -3
  121. package/blocks/dialer/components/call-queue.tsx +1 -1
  122. package/blocks/dialer/components/dialer-screen.tsx +319 -0
  123. package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
  124. package/blocks/dialer/components/live-calls-table.tsx +7 -3
  125. package/blocks/dialer/components/session-start-form.tsx +4 -2
  126. package/blocks/dialer/components/softphone.tsx +3 -1
  127. package/blocks/enrichment/README.md +14 -2
  128. package/blocks/enrichment/block.json +32 -4
  129. package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
  130. package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
  131. package/blocks/enrichment/components/enrich-button.tsx +32 -9
  132. package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
  133. package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
  134. package/blocks/enrichment/components/waterfall-config.tsx +39 -10
  135. package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
  136. package/blocks/forms/README.md +12 -0
  137. package/blocks/forms/block.json +39 -17
  138. package/blocks/forms/components/form-builder.tsx +3 -1
  139. package/blocks/forms/components/form-embed.tsx +3 -1
  140. package/blocks/forms/components/forms-screen.tsx +353 -0
  141. package/blocks/forms/components/submissions-table.tsx +16 -6
  142. package/blocks/inbox/README.md +27 -3
  143. package/blocks/inbox/block.json +14 -2
  144. package/blocks/inbox/components/inbox-list.tsx +7 -3
  145. package/blocks/inbox/components/inbox-screen.tsx +294 -27
  146. package/blocks/inbox/components/reply-composer.tsx +65 -9
  147. package/blocks/inbox/components/tag-and-assign.tsx +4 -2
  148. package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
  149. package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
  150. package/blocks/inbox/lib/inbox-types.ts +47 -1
  151. package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
  152. package/blocks/integrations/README.md +9 -0
  153. package/blocks/integrations/block.json +46 -1
  154. package/blocks/integrations/components/crm-detail.tsx +100 -0
  155. package/blocks/integrations/components/integration-list.tsx +9 -4
  156. package/blocks/integrations/components/integrations-screen.tsx +135 -0
  157. package/blocks/integrations/components/sync-status.tsx +5 -3
  158. package/blocks/integrations/components/webhook-detail.tsx +160 -0
  159. package/blocks/integrations/components/webhook-manager.tsx +13 -8
  160. package/blocks/integrations/lib/integrations-utils.ts +6 -1
  161. package/blocks/intent/README.md +14 -0
  162. package/blocks/intent/block.json +27 -1
  163. package/blocks/intent/components/intent-feed.tsx +5 -2
  164. package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
  165. package/blocks/intent/components/intent-screen.tsx +265 -0
  166. package/blocks/intent/components/keyword-detail.tsx +124 -0
  167. package/blocks/lifecycle/README.md +16 -0
  168. package/blocks/lifecycle/block.json +34 -7
  169. package/blocks/lifecycle/components/account-related.tsx +66 -0
  170. package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
  171. package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
  172. package/blocks/lifecycle/lib/lifecycle.ts +1 -1
  173. package/blocks/lists/README.md +13 -0
  174. package/blocks/lists/block.json +39 -1
  175. package/blocks/lists/components/add-to-list-button.tsx +3 -2
  176. package/blocks/lists/components/list-picker.tsx +5 -1
  177. package/blocks/lists/components/list-table.tsx +11 -5
  178. package/blocks/lists/components/lists-screen.tsx +344 -0
  179. package/blocks/lists/components/suppression-manager.tsx +6 -2
  180. package/blocks/lists/components/suppressions-screen.tsx +255 -0
  181. package/blocks/mailboxes/README.md +17 -0
  182. package/blocks/mailboxes/block.json +65 -17
  183. package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
  184. package/blocks/mailboxes/components/domain-detail.tsx +142 -0
  185. package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
  186. package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
  187. package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
  188. package/blocks/mailboxes/components/warmup-health.tsx +4 -2
  189. package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
  190. package/blocks/newsletter/README.md +13 -0
  191. package/blocks/newsletter/block.json +47 -23
  192. package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
  193. package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
  194. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  195. package/blocks/nurture/README.md +12 -0
  196. package/blocks/nurture/block.json +35 -13
  197. package/blocks/nurture/components/nurture-builder.tsx +3 -1
  198. package/blocks/nurture/components/nurture-enroll.tsx +6 -3
  199. package/blocks/nurture/components/nurture-form.tsx +23 -3
  200. package/blocks/nurture/components/nurture-screen.tsx +283 -0
  201. package/blocks/nurture/lib/nurture-types.ts +1 -1
  202. package/blocks/objects/README.md +17 -0
  203. package/blocks/objects/block.json +29 -16
  204. package/blocks/objects/components/object-not-found.tsx +3 -1
  205. package/blocks/objects/components/object-record-detail.tsx +2 -1
  206. package/blocks/objects/components/object-schema-editor.tsx +3 -1
  207. package/blocks/objects/components/object-table.tsx +13 -3
  208. package/blocks/objects/components/objects-screen.tsx +147 -0
  209. package/blocks/objects/lib/object-types.ts +1 -1
  210. package/blocks/pages/README.md +12 -0
  211. package/blocks/pages/block.json +35 -22
  212. package/blocks/pages/components/brand-kit-editor.tsx +5 -2
  213. package/blocks/pages/components/content-library.tsx +46 -11
  214. package/blocks/pages/components/page-list.tsx +6 -3
  215. package/blocks/pages/components/pages-screen.tsx +154 -0
  216. package/blocks/quotes/README.md +17 -0
  217. package/blocks/quotes/block.json +38 -9
  218. package/blocks/quotes/components/product-picker.tsx +3 -1
  219. package/blocks/quotes/components/quote-list.tsx +7 -3
  220. package/blocks/quotes/components/quote-related.tsx +123 -0
  221. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  222. package/blocks/radar/README.md +10 -0
  223. package/blocks/radar/block.json +41 -1
  224. package/blocks/radar/components/competitor-detail.tsx +117 -0
  225. package/blocks/radar/components/competitor-tracker.tsx +3 -1
  226. package/blocks/radar/components/radar-report.tsx +4 -2
  227. package/blocks/radar/components/radar-screen.tsx +214 -0
  228. package/blocks/radar/components/report-detail.tsx +104 -0
  229. package/blocks/search/README.md +10 -0
  230. package/blocks/search/block.json +16 -1
  231. package/blocks/search/components/filter-builder.tsx +3 -3
  232. package/blocks/search/components/prospect-results.tsx +5 -3
  233. package/blocks/search/components/prospect-search-screen.tsx +235 -0
  234. package/blocks/search/components/saved-searches.tsx +4 -1
  235. package/blocks/search/hooks/use-filter-options.ts +20 -6
  236. package/blocks/search/lib/search-types.ts +2 -0
  237. package/blocks/sequencer/README.md +49 -9
  238. package/blocks/sequencer/block.json +99 -19
  239. package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
  240. package/blocks/sequencer/components/sequence-builder.tsx +211 -46
  241. package/blocks/sequencer/components/sequence-detail.tsx +259 -0
  242. package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
  243. package/blocks/sequencer/components/sequence-list.tsx +33 -8
  244. package/blocks/sequencer/components/sequence-stats.tsx +3 -2
  245. package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
  246. package/blocks/sequencer/components/sequences-screen.tsx +159 -0
  247. package/blocks/sequencer/components/step-editor.tsx +3 -1
  248. package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
  249. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
  250. package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
  251. package/blocks/social-ads/README.md +11 -0
  252. package/blocks/social-ads/block.json +101 -2
  253. package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
  254. package/blocks/social-ads/components/ads-screen.tsx +152 -0
  255. package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
  256. package/blocks/social-ads/components/audience-sync.tsx +11 -7
  257. package/blocks/social-ads/components/campaign-detail.tsx +133 -0
  258. package/blocks/social-ads/components/creative-detail.tsx +150 -0
  259. package/blocks/social-ads/components/creative-gallery.tsx +15 -5
  260. package/blocks/social-ads/components/post-composer.tsx +3 -1
  261. package/blocks/social-ads/components/post-detail.tsx +160 -0
  262. package/blocks/social-ads/components/social-post-list.tsx +11 -3
  263. package/blocks/social-ads/components/social-screen.tsx +160 -0
  264. package/blocks/visitors/README.md +10 -0
  265. package/blocks/visitors/block.json +25 -1
  266. package/blocks/visitors/components/snippet-install.tsx +3 -1
  267. package/blocks/visitors/components/visitor-company-card.tsx +2 -2
  268. package/blocks/visitors/components/visitor-detail.tsx +105 -0
  269. package/blocks/visitors/components/visitor-feed.tsx +11 -2
  270. package/blocks/visitors/components/visitors-screen.tsx +166 -0
  271. package/blocks/webchat/README.md +10 -0
  272. package/blocks/webchat/block.json +21 -7
  273. package/blocks/webchat/components/chat-widget-config.tsx +7 -4
  274. package/blocks/webchat/components/conversation-list.tsx +7 -3
  275. package/blocks/webchat/components/webchat-screen.tsx +245 -0
  276. package/blocks/workbench/README.md +11 -0
  277. package/blocks/workbench/block.json +48 -1
  278. package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
  279. package/blocks/workbench/components/workbench-grid.tsx +9 -3
  280. package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
  281. package/blocks/workbench/components/workbench-screen.tsx +192 -0
  282. package/blocks/workbench/components/workbench-sources.tsx +194 -0
  283. package/blocks/workflows/README.md +13 -0
  284. package/blocks/workflows/block.json +28 -9
  285. package/blocks/workflows/components/run-history.tsx +31 -9
  286. package/blocks/workflows/components/trigger-config.tsx +3 -1
  287. package/blocks/workflows/components/workflow-editor.tsx +3 -1
  288. package/blocks/workflows/components/workflow-list.tsx +13 -6
  289. package/blocks/workflows/components/workflows-screen.tsx +193 -0
  290. package/dist/cli.js +2785 -1637
  291. package/dist/errors.d.mts +34 -0
  292. package/dist/errors.d.ts +34 -0
  293. package/dist/errors.js +46 -0
  294. package/dist/errors.js.map +1 -0
  295. package/dist/errors.mjs +21 -0
  296. package/dist/errors.mjs.map +1 -0
  297. package/dist/index.browser.js +11 -5
  298. package/dist/index.browser.js.map +1 -1
  299. package/dist/index.browser.mjs +11 -5
  300. package/dist/index.browser.mjs.map +1 -1
  301. package/dist/index.d.mts +164 -25
  302. package/dist/index.d.ts +164 -25
  303. package/dist/index.js +11 -5
  304. package/dist/index.js.map +1 -1
  305. package/dist/index.mjs +11 -5
  306. package/dist/index.mjs.map +1 -1
  307. package/dist/react.browser.js +10 -4
  308. package/dist/react.browser.js.map +1 -1
  309. package/dist/react.browser.mjs +10 -4
  310. package/dist/react.browser.mjs.map +1 -1
  311. package/dist/react.d.mts +142 -5
  312. package/dist/react.d.ts +142 -5
  313. package/dist/react.js +10 -4
  314. package/dist/react.js.map +1 -1
  315. package/dist/react.mjs +10 -4
  316. package/dist/react.mjs.map +1 -1
  317. package/dist/types/blocks/add.d.ts +11 -4
  318. package/dist/types/blocks/brand.d.ts +113 -0
  319. package/dist/types/blocks/compose.d.ts +53 -0
  320. package/dist/types/blocks/create.d.ts +25 -8
  321. package/dist/types/blocks/doctor.d.ts +6 -1
  322. package/dist/types/blocks/flows.d.ts +20 -0
  323. package/dist/types/blocks/navfile.d.ts +35 -0
  324. package/dist/types/blocks/paths.d.ts +4 -0
  325. package/dist/types/blocks/prefixes.d.ts +37 -0
  326. package/dist/types/blocks/registry.d.ts +32 -0
  327. package/dist/types/blocks/screens.d.ts +53 -0
  328. package/dist/types/cli.d.ts +2 -0
  329. package/dist/types/errors.d.ts +33 -0
  330. package/dist/types/http.d.ts +2 -18
  331. package/dist/types/version.d.ts +2 -2
  332. package/docs/agents.md +15 -4
  333. package/docs/api/README.md +2 -2
  334. package/docs/api/by-path.md +1 -0
  335. package/docs/api/desk.d.ts +98 -2
  336. package/docs/api/desk.md +123 -1
  337. package/docs/api/inbox.d.ts +10 -2
  338. package/docs/api/inbox.md +10 -2
  339. package/docs/api/seo.d.ts +19 -1
  340. package/docs/api/seo.md +24 -1
  341. package/package.json +16 -2
  342. package/stacks/6sense.json +41 -16
  343. package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
  344. package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
  345. package/stacks/apollo.json +118 -36
  346. package/stacks/calendly.json +60 -0
  347. package/stacks/clay/pages/companies.tsx.tmpl +15 -0
  348. package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
  349. package/stacks/clay.json +59 -26
  350. package/stacks/gong.json +81 -35
  351. package/stacks/hootsuite.json +99 -0
  352. package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
  353. package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
  354. package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
  355. package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
  356. package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
  357. package/stacks/hubspot.json +167 -66
  358. package/stacks/intercom.json +48 -20
  359. package/stacks/mailchimp.json +52 -33
  360. package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
  361. package/stacks/outreach.json +88 -25
  362. package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
  363. package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
  364. package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
  365. package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
  366. package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
  367. package/stacks/pipedrive.json +115 -0
  368. package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
  369. package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
  370. package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
  371. package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
  372. package/stacks/salesforce.json +81 -31
  373. package/stacks/stack.schema.json +6 -1
  374. package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
  375. package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
  376. package/stacks/zoominfo.json +102 -0
  377. package/styles/styles.css +1 -1
  378. package/templates/next-app/README.md.tmpl +55 -8
  379. package/templates/next-app/app/globals.css.tmpl +105 -0
  380. package/templates/next-app/app/layout.tsx.tmpl +18 -4
  381. package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
  382. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  383. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  384. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  385. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  386. package/templates/next-app/dot-gitignore.tmpl +2 -0
  387. package/templates/next-app/lib/nav.ts.tmpl +1 -1
  388. package/templates/next-app/package.json.tmpl +1 -1
@@ -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
+ }
@@ -4,6 +4,25 @@ Event types, weekly working hours, a booking page, a list of booked and recorded
4
4
  rep takes on a booking (confirm, cancel, add a note). Install with `npx @graph8/sdk add appointments`
5
5
  (depends on `crm`).
6
6
 
7
+ ## Screens
8
+
9
+ - `MeetingsScreen` (`components/meetings-screen.tsx`): `<MeetingsScreen />`. Upcoming, Past, Cancelled and Recorded
10
+ meetings; headline numbers (upcoming, awaiting approval); open a booking (deep link `?sel=booking:<uid>`) to accept or
11
+ decline a pending one, add an internal note or cancel (all confirmed, they email attendees), or open a recorded
12
+ meeting for its summary, topics, action items and transcript. **Schedule a meeting** picks an event type, a day and a
13
+ time and books it for an attendee (external, confirmed). Calendar sign-in is only possible in the graph8 app, so the
14
+ screen shows a link-out card (`connectCalendarUrl`).
15
+ - `SchedulingScreen` (`components/scheduling-screen.tsx`): `<SchedulingScreen />`. Event types tab: the list (from
16
+ `GET /event-types`), create, open one (`?id=<event type id>`) to edit or delete it (confirmed) and to try its booking
17
+ page. Working hours tab: weekly hours, date overrides and connected calendars for a schedule.
18
+ - Slots for cross-block glue, all optional: `renderRowActions`, `renderDetailActions`, `extraDetailTabs`,
19
+ `toolbarActions`.
20
+
21
+ The older note that there is no list-event-types operation is out of date: `GET /event-types` (`useEventTypes`) lists
22
+ them, so ids no longer have to be stored. It needs the `campaigns:read` scope and `event-types` in the proxy
23
+ `allowedPrefixes`, next to `appointments` and `inbox`.
24
+
25
+
7
26
  ## The flow, in order
8
27
 
9
28
  1. **Hours** (`AvailabilityEditor`): `GET /appointments/schedules` -> pick `schedule.id` ->
@@ -30,7 +49,7 @@ booking), `booking.uid` (list -> detail -> confirm/cancel/notes), recorded `meet
30
49
  not in the API. `AvailabilityEditor` shows connection state and links out via `connectCalendarUrl`.
31
50
  - Use an API key minted by the host. Appointments act as a person; an organisation key can read and
32
51
  list slots, but anything personal (profile, slot reservation) requires a personal key.
33
- - Scopes: `meetings:read`, `meetings:write`, `meetings:run`, `meetings:delete` (cancel a booking, delete an event type).
52
+ - Scopes: `meetings:read`, `meetings:write`, `meetings:run`, `meetings:delete` (cancel a booking, delete an event type), plus `campaigns:read` for the event-type list.
34
53
 
35
54
  ## Defaults (the agent does not need to ask)
36
55
 
@@ -70,6 +89,7 @@ with the full API key.
70
89
  | `appointments.addBookingNote` | POST /appointments/bookings/{uid}/notes | external | MeetingDetail |
71
90
  | `meetings.listMeetings` | GET /inbox/meetings | read | MeetingList |
72
91
  | `meetings.getMeeting` | GET /inbox/meetings/{id} | read | MeetingDetail |
92
+ | `gtmLaunchHelpers.listEventTypes` | GET /event-types | read | useEventTypes (both screens) |
73
93
 
74
94
  ## Recipes (server side)
75
95
 
@@ -27,6 +27,7 @@
27
27
  "get_meeting_inbox_meetings__meeting_id__get",
28
28
  "list_bookings_appointments_bookings_get",
29
29
  "list_calendars_appointments_calendars_get",
30
+ "list_event_types_event_types_get",
30
31
  "list_meetings_inbox_meetings_get",
31
32
  "list_schedules_appointments_schedules_get",
32
33
  "update_availability_appointments_schedules__schedule_id__availability_patch",
@@ -107,6 +108,36 @@
107
108
  "external",
108
109
  "destructive"
109
110
  ]
111
+ },
112
+ {
113
+ "name": "MeetingsScreen",
114
+ "file": "components/meetings-screen.tsx",
115
+ "purpose": "Finished meetings product: upcoming, past, cancelled and recorded meetings with detail, approvals, notes and a schedule-a-meeting flow.",
116
+ "operations": [
117
+ "list_bookings_appointments_bookings_get"
118
+ ],
119
+ "tiers": [
120
+ "read"
121
+ ]
122
+ },
123
+ {
124
+ "name": "SchedulingScreen",
125
+ "file": "components/scheduling-screen.tsx",
126
+ "purpose": "Finished scheduling product: event types with settings and booking-page preview, plus weekly working hours and calendar status.",
127
+ "operations": [],
128
+ "tiers": []
129
+ },
130
+ {
131
+ "name": "EventTypeOverview",
132
+ "file": "components/event-type-related.tsx",
133
+ "purpose": "Read views of one event type: every field (EventTypeOverview) and the bookings made through it (EventTypeBookings, with MeetingDetail in a sheet).",
134
+ "operations": [
135
+ "get_event_type_appointments_event_types__event_type_id__get",
136
+ "list_bookings_appointments_bookings_get"
137
+ ],
138
+ "tiers": [
139
+ "read"
140
+ ]
110
141
  }
111
142
  ],
112
143
  "hooks": [
@@ -125,6 +156,14 @@
125
156
  "operations": [
126
157
  "get_available_slots_appointments_slots_get"
127
158
  ]
159
+ },
160
+ {
161
+ "name": "useEventTypes",
162
+ "file": "hooks/use-event-types.ts",
163
+ "purpose": "The org's bookable event types (the list operation the editor needs).",
164
+ "operations": [
165
+ "list_event_types_event_types_get"
166
+ ]
128
167
  }
129
168
  ],
130
169
  "lib": [
@@ -174,12 +213,13 @@
174
213
  },
175
214
  {
176
215
  "id": "meetings-scopes",
177
- "description": "Key scopes meetings:read, meetings:write, meetings:run (booking, confirm, notes are tier external) and meetings:delete (cancel, delete event type).",
216
+ "description": "Key scopes meetings:read, meetings:write, meetings:run (booking, confirm, notes are tier external) and meetings:delete (cancel, delete event type), plus campaigns:read for the event-type list (GET /event-types).",
178
217
  "where": "api",
179
218
  "verify": "`g8.api.me.describeCurrentKey()` lists the meetings:* scopes."
180
219
  }
181
220
  ],
182
221
  "scopes": [
222
+ "campaigns:read",
183
223
  "meetings:delete",
184
224
  "meetings:read",
185
225
  "meetings:run",
@@ -213,6 +253,7 @@
213
253
  }
214
254
  ],
215
255
  "peerDependencies": {
216
- "react": ">=18"
256
+ "react": ">=18",
257
+ "lucide-react": ">=0.400.0"
217
258
  }
218
259
  }
@@ -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
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Read views of one event type, as tabs of its full detail: Overview (every field) and Bookings (who booked it).
5
+ *
6
+ * SDK operations, in order:
7
+ * EventTypeOverview: GET /appointments/event-types/{id} (appointments.getEventType) read
8
+ * EventTypeBookings: GET /appointments/bookings?event_type_ids=&skip=&take= (appointments.listBookings) read
9
+ * then MeetingDetail in a side sheet for the booking you open (confirm, cancel and notes
10
+ * are external or destructive there and go through ConfirmSpendDialog).
11
+ */
12
+ import { useState } from "react";
13
+ import { CalendarClock } from "lucide-react";
14
+ import { Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
15
+ import { ApiError } from "@/g8/components/api-error";
16
+ import { EmptyState } from "@/g8/components/empty-state";
17
+ import { KeyValueList } from "@/g8/components/screen/key-value-list";
18
+ import { SectionCard } from "@/g8/components/screen/section-card";
19
+ import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
20
+ import { g8Get } from "@/g8/lib/g8-client";
21
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
22
+ import { formatDateTime, listOf, unwrap, type Booking, type EventType } from "../lib/appointments-types";
23
+ import { MeetingDetail } from "./meeting-detail";
24
+ import { rowActivationProps } from "@/g8/hooks/use-row-activation";
25
+ import { RecordLink } from "@/g8/components/screen/record-link";
26
+
27
+ export function EventTypeOverview({ eventTypeId }: { eventTypeId: number }) {
28
+ const q = useG8Query(async (signal) => unwrap<EventType>(await g8Get(`/appointments/event-types/${eventTypeId}`, undefined, { signal })), [eventTypeId]);
29
+ if (q.error) return <ApiError error={q.error} title="Could not load the event type" onRetry={() => void q.refetch()} />;
30
+ if (!q.data) return <Skeleton className="h-32 w-full" aria-label="Loading event type" />;
31
+ const t = q.data;
32
+ return (
33
+ <SectionCard title="Details">
34
+ <KeyValueList columns={2} items={[
35
+ { label: "Title", value: t.title }, { label: "URL slug", value: t.slug ? `/${t.slug}` : null, mono: true }, { label: "Length", value: t.length ? `${t.length} minutes` : null },
36
+ { label: "Working hours", value: t.schedule?.name }, { label: "Minimum notice", value: t.minimum_booking_notice != null ? `${t.minimum_booking_notice} minutes` : null },
37
+ { label: "Host approval", value: t.requires_confirmation ? "Required" : "Not required" }, { label: "Visibility", value: t.hidden ? "Hidden" : "Bookable" },
38
+ { label: "Scheduling type", value: t.scheduling_type ?? "Single host" }, { label: "Description", value: t.description, wide: true },
39
+ ]} />
40
+ </SectionCard>
41
+ );
42
+ }
43
+
44
+ export function EventTypeBookings({ eventTypeId, pageSize = 20 }: { eventTypeId: number; pageSize?: number }) {
45
+ const [page, setPage] = useState(0);
46
+ const [open, setOpen] = useState<string | null>(null);
47
+ const q = useG8Query(async (signal) => listOf<Booking>(await g8Get("/appointments/bookings", { event_type_ids: String(eventTypeId), skip: page * pageSize, take: pageSize, sort_by: "start_time", sort_order: "desc" }, { signal }), "bookings"), [eventTypeId, page, pageSize]);
48
+ if (q.error) return <ApiError error={q.error} title="Could not load bookings" onRetry={() => void q.refetch()} />;
49
+ if (!q.data) return <Skeleton className="h-32 w-full" aria-label="Loading bookings" />;
50
+ if (q.data.length === 0 && page === 0) return <EmptyState icon={<CalendarClock className="size-8" />} title="No bookings yet" description="Bookings made through this event type appear here." />;
51
+ return (
52
+ <div className="space-y-3">
53
+ <Table>
54
+ <TableHeader><TableRow><TableHead>When</TableHead><TableHead>Attendees</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
55
+ <TableBody>
56
+ {q.data.map((b) => (
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>
59
+ <TableCell className="max-w-64 truncate">{b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees"}</TableCell>
60
+ <TableCell><StatusPill tone={toneForStatus(b.status)} dot>{humanizeStatus(b.status)}</StatusPill></TableCell>
61
+ </TableRow>
62
+ ))}
63
+ </TableBody>
64
+ </Table>
65
+ <div className="flex justify-end gap-2">
66
+ <Button size="sm" variant="outline" disabled={page === 0 || q.loading} onClick={() => setPage((p) => p - 1)}>Previous</Button>
67
+ <Button size="sm" variant="outline" disabled={q.data.length < pageSize || q.loading} onClick={() => setPage((p) => p + 1)}>Next</Button>
68
+ </div>
69
+ <Sheet open={open != null} onOpenChange={(o) => { if (!o) setOpen(null); }}>
70
+ <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
71
+ <SheetHeader><SheetTitle>Booking</SheetTitle><SheetDescription>Accept, decline, note or cancel.</SheetDescription></SheetHeader>
72
+ {open ? <MeetingDetail selection={{ kind: "booking", id: open }} onChanged={() => void q.refetch()} /> : null}
73
+ </SheetContent>
74
+ </Sheet>
75
+ </div>
76
+ );
77
+ }
@@ -19,14 +19,22 @@ 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;
34
+ /** Change it to reload the current page (after a confirm, cancel or booking elsewhere on the screen). */
35
+ refreshKey?: number;
36
+ /** Mark this row as the open one. */
37
+ selectedId?: string | null;
30
38
  className?: string;
31
39
  }
32
40
 
@@ -54,7 +62,7 @@ function useDebouncedText(value: string, ms = 300): string {
54
62
  return v;
55
63
  }
56
64
 
57
- export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20, className }: MeetingListProps) {
65
+ export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
58
66
  const [view, setView] = useState<MeetingView>(initialView);
59
67
  const [page, setPage] = useState(0);
60
68
  const [search, setSearch] = useState("");
@@ -71,7 +79,7 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
71
79
  const items = listOf<Booking>(env, "bookings");
72
80
  return { kind: "booking" as const, items, more: items.length === pageSize };
73
81
  },
74
- [view, page, query, pageSize, now],
82
+ [view, page, query, pageSize, now, refreshKey],
75
83
  );
76
84
 
77
85
  const switchView = (next: MeetingView) => {
@@ -116,17 +124,17 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
116
124
  <TableBody>
117
125
  {result.kind === "booking"
118
126
  ? (result.items as Booking[]).map((b) => (
119
- <TableRow key={b.uid} 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 })) : {})}>
120
128
  <TableCell className="whitespace-nowrap">{formatDateTime(b.start_time)}</TableCell>
121
- <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>
122
130
  <TableCell className="max-w-64 truncate">{attendeeNames(b)}</TableCell>
123
131
  <TableCell className="capitalize">{b.status}</TableCell>
124
132
  </TableRow>
125
133
  ))
126
134
  : (result.items as RecordedMeeting[]).map((m) => (
127
- <TableRow key={m.id} 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 })) : {})}>
128
136
  <TableCell className="whitespace-nowrap">{formatDateTime(m.start_time)}</TableCell>
129
- <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>
130
138
  <TableCell className="max-w-64 truncate">{m.transcript_summary || "No summary yet"}</TableCell>
131
139
  <TableCell className="capitalize">{m.transcript_status ?? "none"}</TableCell>
132
140
  </TableRow>