@graph8/sdk 0.370.0 → 0.374.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 (646) hide show
  1. package/README.md +22 -3
  2. package/blocks/_foundation/README.md +61 -0
  3. package/blocks/_foundation/block.json +57 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
  6. package/blocks/_foundation/components/empty-state.tsx +5 -2
  7. package/blocks/_foundation/components/list-select.tsx +112 -0
  8. package/blocks/_foundation/components/paging-notices.tsx +55 -0
  9. package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
  10. package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
  11. package/blocks/_foundation/components/screen/index.ts +1 -0
  12. package/blocks/_foundation/components/screen/split-view.tsx +1 -1
  13. package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
  14. package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
  15. package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
  16. package/blocks/_foundation/lib/paging.ts +57 -0
  17. package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
  18. package/blocks/_foundation/server/auth.ts +815 -0
  19. package/blocks/agents/README.md +81 -4
  20. package/blocks/agents/block.json +311 -2
  21. package/blocks/agents/components/agent-activity.tsx +112 -0
  22. package/blocks/agents/components/agent-approvals.tsx +135 -0
  23. package/blocks/agents/components/agent-builder.tsx +17 -3
  24. package/blocks/agents/components/agent-conversations.tsx +143 -0
  25. package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
  26. package/blocks/agents/components/agent-operators.tsx +164 -0
  27. package/blocks/agents/components/agent-run-detail.tsx +20 -3
  28. package/blocks/agents/components/agent-runs.tsx +3 -3
  29. package/blocks/agents/components/agent-skills-panel.tsx +7 -7
  30. package/blocks/agents/components/agent-test-history.tsx +121 -0
  31. package/blocks/agents/components/agents-screen.tsx +53 -11
  32. package/blocks/agents/components/assistant-sessions.tsx +150 -0
  33. package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
  34. package/blocks/agents/components/from-template-button.tsx +65 -0
  35. package/blocks/agents/components/knowledge-browser.tsx +107 -0
  36. package/blocks/agents/components/knowledge-detail.tsx +156 -0
  37. package/blocks/agents/components/knowledge-form.tsx +69 -0
  38. package/blocks/agents/components/name-dialog.tsx +32 -0
  39. package/blocks/agents/components/receptionist-button.tsx +46 -0
  40. package/blocks/agents/components/save-as-template-button.tsx +32 -0
  41. package/blocks/agents/components/skill-editor.tsx +147 -0
  42. package/blocks/agents/components/skill-library.tsx +111 -0
  43. package/blocks/agents/lib/agent-types.ts +7 -1
  44. package/blocks/agents/lib/facts.tsx +13 -0
  45. package/blocks/agents/lib/knowledge-utils.ts +49 -0
  46. package/blocks/agents/lib/load-more.tsx +141 -0
  47. package/blocks/agents/lib/skill-utils.ts +91 -0
  48. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
  49. package/blocks/analytics/components/analytics-screen.tsx +16 -3
  50. package/blocks/analytics/components/kpi-tiles.tsx +1 -1
  51. package/blocks/analytics/components/report-builder.tsx +15 -2
  52. package/blocks/appointments/README.md +6 -0
  53. package/blocks/appointments/block.json +27 -0
  54. package/blocks/appointments/components/booking-page.tsx +2 -2
  55. package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
  56. package/blocks/appointments/components/meeting-detail.tsx +2 -2
  57. package/blocks/appointments/components/meeting-list.tsx +29 -13
  58. package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
  59. package/blocks/campaigns/README.md +1 -1
  60. package/blocks/campaigns/block.json +34 -1
  61. package/blocks/campaigns/components/campaign-assets.tsx +1 -1
  62. package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
  63. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  64. package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
  65. package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
  66. package/blocks/campaigns/components/icp-detail.tsx +11 -2
  67. package/blocks/campaigns/components/icp-editor.tsx +8 -3
  68. package/blocks/campaigns/components/persona-detail.tsx +11 -2
  69. package/blocks/campaigns/components/persona-editor.tsx +6 -1
  70. package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
  71. package/blocks/coach/components/call-scorecard.tsx +7 -7
  72. package/blocks/company-intelligence/README.md +132 -0
  73. package/blocks/company-intelligence/block.json +318 -0
  74. package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
  75. package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
  76. package/blocks/company-intelligence/components/company-profile.tsx +68 -0
  77. package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
  78. package/blocks/company-intelligence/components/report-detail.tsx +189 -0
  79. package/blocks/company-intelligence/components/research-reports.tsx +136 -0
  80. package/blocks/company-intelligence/components/research-run.tsx +165 -0
  81. package/blocks/company-intelligence/components/source-detail.tsx +117 -0
  82. package/blocks/company-intelligence/components/source-view.tsx +274 -0
  83. package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
  84. package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
  85. package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
  86. package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
  87. package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
  88. package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
  89. package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
  90. package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
  91. package/blocks/copilot/components/copilot-memory.tsx +6 -2
  92. package/blocks/copilot/components/copilot-panel.tsx +1 -1
  93. package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
  94. package/blocks/crm/README.md +25 -0
  95. package/blocks/crm/block.json +257 -9
  96. package/blocks/crm/components/bulk-update.tsx +119 -0
  97. package/blocks/crm/components/companies-screen.tsx +23 -9
  98. package/blocks/crm/components/company-detail.tsx +2 -2
  99. package/blocks/crm/components/contact-detail.tsx +4 -4
  100. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  101. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  102. package/blocks/crm/components/contact-lookup-link.tsx +85 -0
  103. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  104. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  105. package/blocks/crm/components/contacts-screen.tsx +77 -18
  106. package/blocks/crm/components/dedupe-review.tsx +6 -1
  107. package/blocks/crm/components/import-wizard.tsx +3 -7
  108. package/blocks/crm/components/list-select.tsx +20 -0
  109. package/blocks/crm/components/owner-assign.tsx +132 -0
  110. package/blocks/crm/components/record-engagement-tab.tsx +103 -0
  111. package/blocks/crm/components/record-form.tsx +1 -1
  112. package/blocks/crm/components/record-open-links.tsx +79 -0
  113. package/blocks/crm/components/tag-assign.tsx +112 -0
  114. package/blocks/crm/components/tag-manager.tsx +178 -0
  115. package/blocks/crm/components/tasks-screen.tsx +35 -15
  116. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  117. package/blocks/crm/components/verify-email-button.tsx +93 -0
  118. package/blocks/crm/hooks/use-all-lists.ts +18 -0
  119. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  120. package/blocks/crm/hooks/use-page-query.ts +3 -1
  121. package/blocks/crm/lib/crm-types.ts +14 -0
  122. package/blocks/crm/lib/read-lists.ts +16 -0
  123. package/blocks/crm/lib/record-actions.ts +102 -0
  124. package/blocks/deals/README.md +25 -6
  125. package/blocks/deals/block.json +168 -2
  126. package/blocks/deals/components/close-deal-dialog.tsx +169 -0
  127. package/blocks/deals/components/create-deal-button.tsx +109 -0
  128. package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
  129. package/blocks/deals/components/deal-detail.tsx +7 -5
  130. package/blocks/deals/components/deal-filters.tsx +65 -0
  131. package/blocks/deals/components/deal-line-items.tsx +120 -0
  132. package/blocks/deals/components/deal-notes-tab.tsx +97 -0
  133. package/blocks/deals/components/deal-related.tsx +30 -9
  134. package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
  135. package/blocks/deals/components/deals-screen.tsx +57 -22
  136. package/blocks/deals/components/forecast-view.tsx +4 -4
  137. package/blocks/deals/components/pipeline-board.tsx +8 -6
  138. package/blocks/deals/components/pipeline-editor.tsx +132 -0
  139. package/blocks/deals/components/record-deals-tab.tsx +62 -0
  140. package/blocks/deals/components/stage-editor.tsx +1 -1
  141. package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
  142. package/blocks/deals/lib/deal-extras.ts +156 -0
  143. package/blocks/dialer/README.md +14 -5
  144. package/blocks/dialer/block.json +17 -2
  145. package/blocks/dialer/components/call-contact-button.tsx +26 -9
  146. package/blocks/dialer/components/call-log-screen.tsx +10 -2
  147. package/blocks/dialer/components/call-log.tsx +16 -4
  148. package/blocks/dialer/components/call-queue.tsx +3 -3
  149. package/blocks/dialer/components/dialer-screen.tsx +69 -27
  150. package/blocks/dialer/components/disposition-panel.tsx +1 -1
  151. package/blocks/dialer/components/record-calls-tab.tsx +101 -0
  152. package/blocks/dialer/components/session-start-form.tsx +58 -11
  153. package/blocks/dialer/components/softphone.tsx +1 -1
  154. package/blocks/documents/README.md +86 -0
  155. package/blocks/documents/block.json +378 -0
  156. package/blocks/documents/components/account-bar.tsx +48 -0
  157. package/blocks/documents/components/correspondence-form.tsx +95 -0
  158. package/blocks/documents/components/correspondence-import.tsx +106 -0
  159. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  160. package/blocks/documents/components/documents-screen.tsx +76 -0
  161. package/blocks/documents/components/library-document-detail.tsx +175 -0
  162. package/blocks/documents/components/library-document-form.tsx +105 -0
  163. package/blocks/documents/components/library-folder-form.tsx +74 -0
  164. package/blocks/documents/components/library-panel.tsx +235 -0
  165. package/blocks/documents/components/library-upload.tsx +160 -0
  166. package/blocks/documents/components/question-row.tsx +130 -0
  167. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  168. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  169. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  170. package/blocks/documents/lib/documents-types.ts +252 -0
  171. package/blocks/documents/lib/download.ts +64 -0
  172. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  173. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  174. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  175. package/blocks/email-signatures/README.md +123 -0
  176. package/blocks/email-signatures/block.json +341 -0
  177. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  178. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  179. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  180. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  181. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  182. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  183. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  184. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  185. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  186. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  187. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  188. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  189. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  190. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  191. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  192. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  193. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  194. package/blocks/enrichment/README.md +38 -10
  195. package/blocks/enrichment/block.json +164 -12
  196. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  197. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  198. package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
  199. package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
  200. package/blocks/enrichment/components/enrich-button.tsx +1 -1
  201. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  202. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  203. package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
  204. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  205. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  206. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  207. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  208. package/blocks/enrichment/components/waterfall-config.tsx +27 -7
  209. package/blocks/enrichment/lib/enrichment-types.ts +51 -0
  210. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  211. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
  212. package/blocks/forms/README.md +10 -0
  213. package/blocks/forms/block.json +107 -0
  214. package/blocks/forms/components/form-activity.tsx +71 -0
  215. package/blocks/forms/components/form-backfill.tsx +108 -0
  216. package/blocks/forms/components/form-builder.tsx +1 -1
  217. package/blocks/forms/components/form-embed.tsx +1 -1
  218. package/blocks/forms/components/form-groups.tsx +151 -0
  219. package/blocks/forms/components/form-mapping-editor.tsx +6 -9
  220. package/blocks/forms/components/form-route-history.tsx +72 -0
  221. package/blocks/forms/components/form-saved-views.tsx +81 -0
  222. package/blocks/forms/components/forms-screen.tsx +57 -20
  223. package/blocks/forms/components/more-rows.tsx +30 -0
  224. package/blocks/forms/components/submissions-export.tsx +67 -0
  225. package/blocks/forms/components/submissions-table.tsx +15 -6
  226. package/blocks/forms/lib/forms-extra.ts +67 -0
  227. package/blocks/inbox/README.md +45 -4
  228. package/blocks/inbox/block.json +145 -8
  229. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  230. package/blocks/inbox/components/compose-message.tsx +193 -0
  231. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  232. package/blocks/inbox/components/inbox-search.tsx +106 -0
  233. package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
  234. package/blocks/inbox/components/tag-manager.tsx +136 -0
  235. package/blocks/inbox/components/thread-actions.tsx +133 -0
  236. package/blocks/inbox/components/thread-badge.tsx +2 -2
  237. package/blocks/inbox/components/thread-view.tsx +3 -1
  238. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  239. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  240. package/blocks/integrations/README.md +54 -1
  241. package/blocks/integrations/block.json +189 -15
  242. package/blocks/integrations/components/ad-accounts.tsx +82 -0
  243. package/blocks/integrations/components/connection-health.tsx +59 -0
  244. package/blocks/integrations/components/crm-detail.tsx +20 -1
  245. package/blocks/integrations/components/crm-records.tsx +103 -0
  246. package/blocks/integrations/components/field-mappings.tsx +61 -0
  247. package/blocks/integrations/components/integration-list.tsx +4 -4
  248. package/blocks/integrations/components/integrations-screen.tsx +20 -3
  249. package/blocks/integrations/components/migration-panel.tsx +133 -0
  250. package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
  251. package/blocks/integrations/components/sync-status.tsx +6 -6
  252. package/blocks/integrations/components/sync-triggers.tsx +134 -0
  253. package/blocks/integrations/components/webhook-detail.tsx +26 -7
  254. package/blocks/integrations/components/webhook-manager.tsx +30 -17
  255. package/blocks/integrations/components/workspace-tools.tsx +127 -0
  256. package/blocks/integrations/lib/crm-utils.ts +39 -0
  257. package/blocks/integrations/lib/facts.tsx +13 -0
  258. package/blocks/integrations/lib/integrations-utils.ts +5 -0
  259. package/blocks/integrations/lib/load-more.tsx +141 -0
  260. package/blocks/intent/components/account-intent-score.tsx +5 -5
  261. package/blocks/intent/components/intent-feed.tsx +8 -8
  262. package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
  263. package/blocks/intent/lib/intent-utils.ts +4 -4
  264. package/blocks/lifecycle/components/account-related.tsx +8 -1
  265. package/blocks/lifecycle/components/churn-list.tsx +5 -2
  266. package/blocks/lifecycle/components/customer-table.tsx +1 -1
  267. package/blocks/lists/README.md +7 -1
  268. package/blocks/lists/block.json +31 -3
  269. package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
  270. package/blocks/lists/components/add-to-list-button.tsx +17 -7
  271. package/blocks/lists/components/list-picker.tsx +13 -3
  272. package/blocks/lists/components/list-table.tsx +1 -1
  273. package/blocks/lists/components/lists-screen.tsx +4 -2
  274. package/blocks/lists/components/record-lists-tab.tsx +99 -0
  275. package/blocks/lists/components/suppression-manager.tsx +27 -6
  276. package/blocks/lists/components/suppressions-screen.tsx +21 -12
  277. package/blocks/mailboxes/block.json +4 -0
  278. package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
  279. package/blocks/mailboxes/components/domain-detail.tsx +10 -4
  280. package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
  281. package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
  282. package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
  283. package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
  284. package/blocks/mailboxes/components/warmup-health.tsx +20 -20
  285. package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
  286. package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
  287. package/blocks/newsletter/README.md +12 -0
  288. package/blocks/newsletter/block.json +117 -0
  289. package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
  290. package/blocks/newsletter/components/issue-versions.tsx +93 -0
  291. package/blocks/newsletter/components/more-rows.tsx +30 -0
  292. package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
  293. package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
  294. package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
  295. package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
  296. package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
  297. package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
  298. package/blocks/newsletter/components/subscriber-table.tsx +5 -7
  299. package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
  300. package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
  301. package/blocks/nurture/components/nurture-builder.tsx +10 -2
  302. package/blocks/nurture/components/nurture-enroll.tsx +6 -7
  303. package/blocks/nurture/components/nurture-form.tsx +5 -8
  304. package/blocks/nurture/components/nurture-screen.tsx +16 -5
  305. package/blocks/nurture/components/nurture-stats.tsx +5 -4
  306. package/blocks/objects/README.md +1 -1
  307. package/blocks/objects/block.json +13 -5
  308. package/blocks/objects/components/attribute-field-input.tsx +14 -1
  309. package/blocks/objects/components/object-record-detail.tsx +8 -1
  310. package/blocks/objects/components/object-schema-editor.tsx +1 -1
  311. package/blocks/objects/components/record-roles-panel.tsx +15 -5
  312. package/blocks/pages/README.md +14 -0
  313. package/blocks/pages/block.json +123 -2
  314. package/blocks/pages/components/more-rows.tsx +30 -0
  315. package/blocks/pages/components/page-ai-tools.tsx +196 -0
  316. package/blocks/pages/components/page-analytics.tsx +176 -0
  317. package/blocks/pages/components/page-clone.tsx +99 -0
  318. package/blocks/pages/components/page-domains.tsx +167 -0
  319. package/blocks/pages/components/page-editor.tsx +1 -1
  320. package/blocks/pages/components/page-generation-status.tsx +48 -0
  321. package/blocks/pages/components/page-tracking.tsx +137 -0
  322. package/blocks/pages/components/pages-screen.tsx +57 -16
  323. package/blocks/pages/components/seo-report.tsx +6 -1
  324. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  325. package/blocks/pages/lib/studio.ts +7 -0
  326. package/blocks/phone-numbers/README.md +74 -0
  327. package/blocks/phone-numbers/block.json +262 -0
  328. package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
  329. package/blocks/phone-numbers/components/assign-number.tsx +104 -0
  330. package/blocks/phone-numbers/components/number-search.tsx +239 -0
  331. package/blocks/phone-numbers/components/number-settings.tsx +134 -0
  332. package/blocks/phone-numbers/components/number-usage.tsx +97 -0
  333. package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
  334. package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
  335. package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
  336. package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
  337. package/blocks/phone-numbers/components/release-number.tsx +126 -0
  338. package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
  339. package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
  340. package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
  341. package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
  342. package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
  343. package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
  344. package/blocks/pickers/README.md +23 -0
  345. package/blocks/pickers/block.json +103 -0
  346. package/blocks/pickers/components/company-picker.tsx +90 -0
  347. package/blocks/pickers/components/contact-picker.tsx +131 -0
  348. package/blocks/pickers/components/member-picker.tsx +99 -0
  349. package/blocks/pickers/components/search-picker.tsx +98 -0
  350. package/blocks/pickers/hooks/use-current-member.ts +104 -0
  351. package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
  352. package/blocks/pickers/lib/use-debounced.ts +13 -0
  353. package/blocks/programmatic-pages/README.md +82 -0
  354. package/blocks/programmatic-pages/block.json +292 -0
  355. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  356. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  357. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  358. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  359. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  360. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  361. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  362. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  363. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  364. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  365. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  366. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  367. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  368. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  369. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  370. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  371. package/blocks/quotes/README.md +38 -1
  372. package/blocks/quotes/block.json +170 -6
  373. package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
  374. package/blocks/quotes/components/manual-products.tsx +144 -0
  375. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  376. package/blocks/quotes/components/quote-builder.tsx +1 -1
  377. package/blocks/quotes/components/quote-documents.tsx +94 -0
  378. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  379. package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
  380. package/blocks/quotes/components/quote-list.tsx +28 -8
  381. package/blocks/quotes/components/quote-related.tsx +3 -2
  382. package/blocks/quotes/components/quote-settings.tsx +184 -0
  383. package/blocks/quotes/components/quote-templates.tsx +189 -0
  384. package/blocks/quotes/components/quote-viewer.tsx +23 -5
  385. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  386. package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
  387. package/blocks/quotes/lib/quote-admin.ts +264 -0
  388. package/blocks/radar/components/competitor-battle-card.tsx +1 -1
  389. package/blocks/radar/components/radar-overview.tsx +4 -4
  390. package/blocks/radar/components/radar-screen.tsx +2 -2
  391. package/blocks/sequencer/README.md +61 -0
  392. package/blocks/sequencer/block.json +200 -6
  393. package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
  394. package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
  395. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  396. package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
  397. package/blocks/sequencer/components/sequence-builder.tsx +32 -23
  398. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  399. package/blocks/sequencer/components/sequence-detail.tsx +10 -2
  400. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  401. package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
  402. package/blocks/sequencer/components/sequence-list.tsx +1 -1
  403. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  404. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  405. package/blocks/sequencer/components/sequence-stats.tsx +8 -8
  406. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  407. package/blocks/sequencer/components/sequences-screen.tsx +39 -6
  408. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  409. package/blocks/sequencer/components/step-editor.tsx +21 -12
  410. package/blocks/sequencer/components/step-templates.tsx +224 -0
  411. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  412. package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
  413. package/blocks/signals/README.md +39 -0
  414. package/blocks/signals/block.json +287 -0
  415. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  416. package/blocks/signals/components/listener-form.tsx +184 -0
  417. package/blocks/signals/components/matched-posts.tsx +195 -0
  418. package/blocks/signals/components/post-engagements.tsx +114 -0
  419. package/blocks/signals/components/record-social-tab.tsx +49 -0
  420. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  421. package/blocks/signals/components/social-settings.tsx +128 -0
  422. package/blocks/signals/components/source-lists.tsx +83 -0
  423. package/blocks/signals/components/top-engagers.tsx +68 -0
  424. package/blocks/signals/components/x-integration.tsx +129 -0
  425. package/blocks/signals/lib/signals-types.ts +228 -0
  426. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  427. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  428. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  429. package/blocks/social-ads/README.md +8 -0
  430. package/blocks/social-ads/block.json +137 -4
  431. package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
  432. package/blocks/social-ads/components/audience-sync.tsx +6 -2
  433. package/blocks/social-ads/components/campaign-ads.tsx +166 -0
  434. package/blocks/social-ads/components/campaign-detail.tsx +6 -1
  435. package/blocks/social-ads/components/campaign-editor.tsx +134 -0
  436. package/blocks/social-ads/components/creative-concepts.tsx +74 -0
  437. package/blocks/social-ads/components/creative-detail.tsx +6 -1
  438. package/blocks/social-ads/components/creative-gallery.tsx +20 -2
  439. package/blocks/social-ads/components/creative-revisions.tsx +57 -0
  440. package/blocks/social-ads/components/creative-tools.tsx +176 -0
  441. package/blocks/social-ads/components/post-composer.tsx +4 -2
  442. package/blocks/social-ads/components/post-detail.tsx +3 -0
  443. package/blocks/social-ads/components/social-ideas.tsx +64 -0
  444. package/blocks/social-ads/components/social-performance.tsx +80 -0
  445. package/blocks/social-ads/components/social-screen.tsx +33 -6
  446. package/blocks/social-ads/lib/ads-extra.ts +67 -0
  447. package/blocks/surveys/README.md +91 -0
  448. package/blocks/surveys/block.json +299 -0
  449. package/blocks/surveys/components/account-nps.tsx +69 -0
  450. package/blocks/surveys/components/contact-survey-history.tsx +52 -0
  451. package/blocks/surveys/components/org-summary.tsx +82 -0
  452. package/blocks/surveys/components/sending-domains.tsx +113 -0
  453. package/blocks/surveys/components/survey-form.tsx +126 -0
  454. package/blocks/surveys/components/survey-responses.tsx +145 -0
  455. package/blocks/surveys/components/survey-results.tsx +72 -0
  456. package/blocks/surveys/components/survey-send-history.tsx +58 -0
  457. package/blocks/surveys/components/survey-send.tsx +162 -0
  458. package/blocks/surveys/components/survey-triggers.tsx +166 -0
  459. package/blocks/surveys/components/surveys-screen.tsx +234 -0
  460. package/blocks/surveys/lib/surveys-types.ts +228 -0
  461. package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
  462. package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
  463. package/blocks/surveys/recipes/survey-score-report.ts +27 -0
  464. package/blocks/visitors/components/snippet-install.tsx +18 -9
  465. package/blocks/visitors/components/traffic-chart.tsx +7 -6
  466. package/blocks/visitors/components/visitor-company-card.tsx +24 -14
  467. package/blocks/visitors/components/visitor-feed.tsx +5 -5
  468. package/blocks/web-analytics/README.md +102 -0
  469. package/blocks/web-analytics/block.json +292 -0
  470. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  471. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  472. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  473. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  474. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  475. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  476. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  477. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  478. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  479. package/blocks/web-analytics/components/report-card.tsx +71 -0
  480. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  481. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  482. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  483. package/blocks/web-analytics/lib/page-path.ts +14 -0
  484. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  485. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  486. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  487. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  488. package/blocks/webchat/components/chat-widget-config.tsx +1 -1
  489. package/blocks/webchat/components/conversation-list.tsx +1 -1
  490. package/blocks/workbench/README.md +54 -2
  491. package/blocks/workbench/block.json +234 -5
  492. package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
  493. package/blocks/workbench/components/run-progress.tsx +1 -1
  494. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  495. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  496. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  497. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  498. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  499. package/blocks/workbench/components/workbench-grid.tsx +71 -8
  500. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  501. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  502. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  503. package/blocks/workbench/components/workbench-screen.tsx +65 -22
  504. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  505. package/blocks/workbench/components/workbench-table-list.tsx +14 -2
  506. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  507. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  508. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  509. package/blocks/workbench/lib/api-status.ts +8 -0
  510. package/blocks/workbench/lib/automation-types.ts +257 -0
  511. package/blocks/workflows/components/workflow-editor.tsx +1 -1
  512. package/dist/cli.js +744 -394
  513. package/dist/index.browser.js +1 -1
  514. package/dist/index.browser.js.map +1 -1
  515. package/dist/index.browser.mjs +1 -1
  516. package/dist/index.browser.mjs.map +1 -1
  517. package/dist/index.d.mts +2 -2
  518. package/dist/index.d.ts +2 -2
  519. package/dist/index.js +1 -1
  520. package/dist/index.js.map +1 -1
  521. package/dist/index.mjs +1 -1
  522. package/dist/index.mjs.map +1 -1
  523. package/dist/react.browser.js +1 -1
  524. package/dist/react.browser.js.map +1 -1
  525. package/dist/react.browser.mjs +1 -1
  526. package/dist/react.browser.mjs.map +1 -1
  527. package/dist/react.d.mts +2 -2
  528. package/dist/react.d.ts +2 -2
  529. package/dist/react.js +1 -1
  530. package/dist/react.js.map +1 -1
  531. package/dist/react.mjs +1 -1
  532. package/dist/react.mjs.map +1 -1
  533. package/dist/types/blocks/auth.d.ts +45 -0
  534. package/dist/types/blocks/brand.d.ts +11 -2
  535. package/dist/types/blocks/create.d.ts +11 -4
  536. package/dist/types/blocks/doctor.d.ts +1 -0
  537. package/dist/types/blocks/flows.d.ts +7 -1
  538. package/dist/types/blocks/orgBrand.d.ts +34 -0
  539. package/dist/types/cli.d.ts +3 -0
  540. package/dist/types/version.d.ts +2 -2
  541. package/docs/api/desk.d.ts +2 -2
  542. package/docs/api/desk.md +3 -3
  543. package/docs/api/forms.md +1 -1
  544. package/package.json +1 -1
  545. package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
  546. package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
  547. package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
  548. package/stacks/6sense.json +37 -4
  549. package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
  550. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  551. package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
  552. package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
  553. package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
  554. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
  555. package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
  556. package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
  557. package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
  558. package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
  559. package/stacks/apollo.json +68 -9
  560. package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
  561. package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
  562. package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
  563. package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
  564. package/stacks/calendly.json +8 -4
  565. package/stacks/clay/pages/companies.tsx.tmpl +17 -1
  566. package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
  567. package/stacks/clay/pages/lists.tsx.tmpl +24 -0
  568. package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
  569. package/stacks/clay.json +16 -3
  570. package/stacks/gong/pages/calls.tsx.tmpl +23 -0
  571. package/stacks/gong/pages/companies.tsx.tmpl +22 -0
  572. package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
  573. package/stacks/gong/pages/lists.tsx.tmpl +12 -0
  574. package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
  575. package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
  576. package/stacks/gong.json +23 -6
  577. package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
  578. package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
  579. package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
  580. package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
  581. package/stacks/hootsuite.json +19 -4
  582. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  583. package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
  584. package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
  585. package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
  586. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  587. package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
  588. package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
  589. package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
  590. package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
  591. package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
  592. package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
  593. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  594. package/stacks/hubspot.json +66 -7
  595. package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
  596. package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
  597. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
  598. package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
  599. package/stacks/intercom.json +18 -4
  600. package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
  601. package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
  602. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  603. package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
  604. package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
  605. package/stacks/mailchimp.json +22 -6
  606. package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
  607. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  608. package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
  609. package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
  610. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
  611. package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
  612. package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
  613. package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
  614. package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
  615. package/stacks/outreach.json +38 -9
  616. package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
  617. package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
  618. package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
  619. package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
  620. package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
  621. package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
  622. package/stacks/pipedrive.json +14 -2
  623. package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
  624. package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
  625. package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
  626. package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
  627. package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
  628. package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
  629. package/stacks/salesforce.json +17 -3
  630. package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
  631. package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
  632. package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
  633. package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
  634. package/stacks/zoominfo.json +36 -3
  635. package/styles/styles.css +1 -1
  636. package/templates/auth/auth-route.ts.tmpl +16 -0
  637. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  638. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  639. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  640. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  641. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  642. package/templates/auth/middleware.ts.tmpl +14 -0
  643. package/templates/next-app/README.md.tmpl +5 -0
  644. package/templates/next-app/app/globals.css.tmpl +349 -64
  645. package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
  646. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
@@ -0,0 +1,195 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SignatureMailboxes: every mailbox with the signature it sends, its status, and the actions on it.
5
+ *
6
+ * SDK operations (the lists come from the shared hooks: listMailboxes + listSignatureDeployments, listSignatureTemplates,
7
+ * listSignatureMembers, getSignatureSettings):
8
+ * 1. pushSignature (POST /signatures/mailboxes/{mailbox_id}/push) write queue a re-compile and re-install of one mailbox
9
+ * 2. pushSignatures (POST /signatures/mailboxes/push) write the same for the selected mailboxes; all or none (422)
10
+ * 3. restoreSignature (POST /signatures/mailboxes/{mailbox_id}/restore) write put back the signature the mailbox had before takeover
11
+ * 4. changeSignatureSource (POST /signatures/mailboxes/{mailbox_id}/source) write what the mailbox sends: org default, a template, or its own
12
+ * Every one of them changes the signature a mailbox SENDS, so each goes through one ConfirmSpendDialog (external) that names
13
+ * the real number of mailboxes. Per-mailbox writes send the revision that was read (409 on a stale screen, then a refresh).
14
+ * Assigning an employee to a mailbox (the first step) opens SignatureIdentityForm in a sheet. Selection only holds mailboxes that
15
+ * still exist and are managed, and is cleared after a bulk push. Mailboxes with no signature yet are listed too; a key that
16
+ * cannot read mailboxes still sees the ones that have one, with a note. A deployment waiting for the mail admin's signature
17
+ * permission (needs_admin_grant) shows a link-out to the platform app, whose name and address come from useBrand.
18
+ */
19
+
20
+ import { useState } from "react";
21
+ import { RefreshCw, RotateCcw, UserPlus } from "lucide-react";
22
+ import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
23
+ import { ApiError } from "@/g8/components/api-error";
24
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
25
+ import { EmptyState } from "@/g8/components/empty-state";
26
+ import { FirstNotice } from "@/g8/components/paging-notices";
27
+ import { FormSheet } from "@/g8/components/screen/form-sheet";
28
+ import { StatusPill, humanizeStatus } from "@/g8/components/screen/status-pill";
29
+ import { Toolbar } from "@/g8/components/screen/toolbar";
30
+ import { g8Post } from "@/g8/lib/g8-client";
31
+ import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
32
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
33
+ import { plural } from "@/g8/lib/plural";
34
+ import { MAILBOX_LIMIT, useSignatureMailboxes, useSignatureMembers, useSignaturePolicy, useSignatureTemplates } from "../hooks/use-signatures-data";
35
+ import { SOURCE_CHOICES, formatWhen, isConflict, isManaged, memberLabel, statusTone, templateName } from "../lib/signatures-types";
36
+ import type { SignatureMailbox, SourceChoice } from "../lib/signatures-types";
37
+ import { SignatureIdentityForm } from "./signature-identity-form";
38
+
39
+ type Pending =
40
+ | { kind: "push"; mailbox: SignatureMailbox }
41
+ | { kind: "restore"; mailbox: SignatureMailbox }
42
+ | { kind: "bulk"; mailboxes: SignatureMailbox[] }
43
+ | { kind: "source"; mailbox: SignatureMailbox; source: SourceChoice; templateId: string };
44
+
45
+ interface Outcome { status?: string; message?: string | null }
46
+
47
+ const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
48
+ const FILTERS = [{ value: "", label: "All mailboxes" }, { value: "attention", label: "Needs attention" }, { value: "managed", label: "Managed" }, { value: "unmanaged", label: "Not managed" }];
49
+
50
+ export function SignatureMailboxes() {
51
+ const brand = useBrand();
52
+ const box = useSignatureMailboxes();
53
+ const templates = useSignatureTemplates();
54
+ const members = useSignatureMembers();
55
+ const policy = useSignaturePolicy();
56
+ const [search, setSearch] = useState("");
57
+ const [filter, setFilter] = useState("");
58
+ const [picked, setPicked] = useState<number[]>([]);
59
+ const [assigning, setAssigning] = useState<SignatureMailbox | null>(null);
60
+ const [sourcing, setSourcing] = useState<SignatureMailbox | null>(null);
61
+ const [srcChoice, setSrcChoice] = useState<SourceChoice>("org");
62
+ const [srcTemplate, setSrcTemplate] = useState("");
63
+ const [pending, setPending] = useState<Pending | null>(null);
64
+ const [outcome, setOutcome] = useState<string | null>(null);
65
+
66
+ const run = useG8Mutation(async (p: Pending): Promise<Outcome> => {
67
+ if (p.kind === "bulk") return (await g8Post<Outcome>("/signatures/mailboxes/push", { mailbox_ids: p.mailboxes.map((m) => m.id) })).data;
68
+ const base = `/signatures/mailboxes/${p.mailbox.id}`;
69
+ const version = p.mailbox.deployment?.revision ?? 0;
70
+ if (p.kind === "push") return (await g8Post<Outcome>(`${base}/push`, { version })).data;
71
+ if (p.kind === "restore") return (await g8Post<Outcome>(`${base}/restore`, { version })).data;
72
+ return (await g8Post<Outcome>(`${base}/source`, { version, source: p.source, ...(p.source === "template" ? { template_id: p.templateId } : {}) })).data;
73
+ }, {
74
+ onSuccess: (r, p) => {
75
+ setPending(null);
76
+ setSourcing(null);
77
+ setPicked([]);
78
+ setOutcome(r.message ? `${r.status ?? "Done"}: ${r.message}` : `Queued: ${p.kind === "bulk" ? plural(p.mailboxes.length, "mailbox") : p.mailbox.email}.`);
79
+ },
80
+ onError: (e) => { setPending(null); if (isConflict(e)) box.refetch(); },
81
+ invalidates: [["signatures", "deployments"]],
82
+ });
83
+
84
+ const q = search.trim().toLowerCase();
85
+ const memberName = (id: string | undefined): string => (id ? memberLabel(members.data?.find((m) => m.id === id)) : "");
86
+ const shown = box.mailboxes.filter((m) => {
87
+ const d = m.deployment;
88
+ const okFilter = filter === "" || (filter === "attention" && Boolean(d?.alert)) || (filter === "managed" && isManaged(d)) || (filter === "unmanaged" && !isManaged(d));
89
+ return okFilter && (!q || `${m.email} ${memberName(d?.assignment?.member_id)}`.toLowerCase().includes(q));
90
+ });
91
+ const selectable = shown.filter((m) => isManaged(m.deployment));
92
+ const selected = box.mailboxes.filter((m) => picked.includes(m.id) && isManaged(m.deployment));
93
+ const where = (m: SignatureMailbox): string => {
94
+ const a = m.deployment?.assignment;
95
+ if (!isManaged(m.deployment)) return "Its own signature";
96
+ const named = (id: string | null | undefined): string => templateName(templates.data?.find((t) => t.id === id));
97
+ if (a?.template_id) return templates.data ? named(a.template_id) : "";
98
+ const dept = members.data?.find((x) => x.id === a?.member_id)?.department;
99
+ const deptTemplate = dept ? policy.data?.data.department_templates?.[dept] : undefined;
100
+ if (deptTemplate) return `${named(deptTemplate)} (department default)`;
101
+ return policy.data?.data.default_template_id ? `${named(policy.data.data.default_template_id)} (organisation default)` : "No default template is set";
102
+ };
103
+
104
+ const confirmText = (p: Pending): { title: string; body: string; count: number; label: string } => {
105
+ if (p.kind === "bulk") return { title: `Push the signature to ${plural(p.mailboxes.length, "mailbox")}?`, body: "Each mailbox is recompiled and its signature installed again.", count: p.mailboxes.length, label: "Push signatures" };
106
+ if (p.kind === "push") return { title: `Push the signature to ${p.mailbox.email}?`, body: "The mailbox is recompiled and its signature installed again.", count: 1, label: "Push signature" };
107
+ if (p.kind === "restore") return { title: `Restore the old signature of ${p.mailbox.email}?`, body: "The signature this mailbox had before takeover is put back and it is no longer managed. If it was edited elsewhere since, nothing is overwritten. Refused while the policy is enforced.", count: 1, label: "Restore signature" };
108
+ const what = p.source === "template" ? templateName(templates.data?.find((t) => t.id === p.templateId)) : p.source === "org" ? "the organisation or department default" : "its own signature";
109
+ return { title: `Make ${p.mailbox.email} send ${what}?`, body: "New emails from this mailbox carry the change.", count: 1, label: "Change signature" };
110
+ };
111
+ const text = pending ? confirmText(pending) : null;
112
+
113
+ const body = box.error ? <ApiError error={box.error} title="Could not load mailboxes" onRetry={box.refetch} /> : box.loading ? (
114
+ <div aria-busy="true" className="grid gap-2"><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>
115
+ ) : shown.length === 0 ? (
116
+ <EmptyState title={box.mailboxes.length === 0 ? "No mailboxes yet" : "No mailboxes match"} description={box.mailboxes.length === 0 ? "Connect or buy a mailbox first, then give it a managed signature here." : "Try another search or filter."} />
117
+ ) : (
118
+ <>
119
+ <p role="status" className="text-muted-foreground mb-2 text-xs">{shown.length === box.mailboxes.length ? `All ${plural(box.mailboxes.length, "mailbox")}` : `${shown.length.toLocaleString()} of ${plural(box.mailboxes.length, "mailbox")}`}</p>
120
+ <Table>
121
+ <TableHeader>
122
+ <TableRow>
123
+ <TableHead className="w-8"><input type="checkbox" aria-label="Select all shown mailboxes" checked={selectable.length > 0 && selectable.every((m) => picked.includes(m.id))} onChange={(e) => setPicked(e.target.checked ? selectable.map((m) => m.id) : [])} /></TableHead>
124
+ <TableHead>Mailbox</TableHead><TableHead>Employee</TableHead><TableHead>Signature from</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Actions</TableHead>
125
+ </TableRow>
126
+ </TableHeader>
127
+ <TableBody>
128
+ {shown.map((m) => {
129
+ const d = m.deployment;
130
+ const managed = isManaged(d);
131
+ return (
132
+ <TableRow key={m.id}>
133
+ <TableCell><input type="checkbox" aria-label={`Select ${m.email}`} disabled={!managed} checked={picked.includes(m.id)} onChange={(e) => setPicked(e.target.checked ? [...picked, m.id] : picked.filter((id) => id !== m.id))} /></TableCell>
134
+ <TableCell><span className="font-medium">{m.email}</span>{m.display_name ? <span className="text-muted-foreground block text-xs">{m.display_name}</span> : null}</TableCell>
135
+ <TableCell>{memberName(d?.assignment?.member_id) || <span className="text-muted-foreground">None</span>}</TableCell>
136
+ <TableCell>{where(m)}</TableCell>
137
+ <TableCell>
138
+ {d ? <StatusPill tone={statusTone(d)} title={d.error ?? undefined}>{humanizeStatus(d.status)}</StatusPill> : <StatusPill tone="neutral">Not managed</StatusPill>}
139
+ {d?.updated_at ? <span className="text-muted-foreground block text-xs">{formatWhen(d.updated_at)}</span> : null}
140
+ </TableCell>
141
+ <TableCell className="text-right">
142
+ <div className="flex flex-wrap justify-end gap-1">
143
+ <Button size="sm" variant="outline" aria-label={`${d ? "Change employee for" : "Assign a signature to"} ${m.email}`} onClick={() => setAssigning(m)}><UserPlus className="size-4" />{d ? "Employee" : "Assign"}</Button>
144
+ {managed ? <Button size="sm" variant="outline" aria-label={`Push signature to ${m.email}`} onClick={() => setPending({ kind: "push", mailbox: m })}><RefreshCw className="size-4" />Push</Button> : null}
145
+ {managed ? <Button size="sm" variant="ghost" aria-label={`Change source for ${m.email}`} onClick={() => { setSourcing(m); setSrcChoice("org"); setSrcTemplate(""); }}>Source</Button> : null}
146
+ {managed ? <Button size="sm" variant="ghost" aria-label={`Restore the old signature of ${m.email}`} onClick={() => setPending({ kind: "restore", mailbox: m })}><RotateCcw className="size-4" />Restore</Button> : null}
147
+ </div>
148
+ </TableCell>
149
+ </TableRow>
150
+ );
151
+ })}
152
+ </TableBody>
153
+ </Table>
154
+ </>
155
+ );
156
+
157
+ return (
158
+ <div className="grid gap-3">
159
+ <Toolbar
160
+ search={{ value: search, onChange: setSearch, placeholder: "Search mailboxes" }}
161
+ filters={<select aria-label="Filter mailboxes" className={SELECT} value={filter} onChange={(e) => setFilter(e.target.value)}>{FILTERS.map((f) => <option key={f.value} value={f.value}>{f.label}</option>)}</select>}
162
+ actions={<Button size="sm" disabled={selected.length === 0} onClick={() => setPending({ kind: "bulk", mailboxes: selected })}>Push to {plural(selected.length, "selected mailbox", "selected mailboxes")}</Button>}
163
+ />
164
+ {box.mailboxesError ? <ApiError compact error={box.mailboxesError} title="Mailboxes without a signature are not listed" onRetry={box.refetch} /> : null}
165
+ {box.mailboxes.some((m) => m.deployment?.status === "needs_admin_grant") ? (
166
+ <p role="note" className="rounded-md border p-3 text-sm">
167
+ Some mailboxes cannot be updated until a mail admin grants signature access. Do it in the <a className="underline" href={brandUrl(brand, "/studio/settings?tab=integrations")} target="_blank" rel="noreferrer">{brand.linkLabel} integrations settings</a>, then push them again.
168
+ </p>
169
+ ) : null}
170
+ {outcome ? <p role="status" className="text-sm">{outcome}</p> : null}
171
+ {run.error ? (isConflict(run.error) ? <ApiError error={run.error} title="This mailbox changed since the page loaded. It was refreshed: try again" /> : <ApiError error={run.error} title="Could not change the signature" />) : null}
172
+ {body}
173
+ <FirstNotice shown={box.mailboxes.length} limit={MAILBOX_LIMIT} noun="mailboxes" force={box.truncated} />
174
+ <FormSheet open={assigning !== null} onOpenChange={(o) => !o && setAssigning(null)} title={assigning?.deployment ? "Change the employee" : "Assign a signature"} description="Bind an employee to this mailbox. Their details fill the template.">
175
+ {assigning ? <SignatureIdentityForm key={assigning.id} mailbox={assigning} onDone={() => { setAssigning(null); setOutcome(`Queued the first install for ${assigning.email}.`); }} onCancel={() => setAssigning(null)} /> : null}
176
+ </FormSheet>
177
+ <FormSheet open={sourcing !== null} onOpenChange={(o) => !o && setSourcing(null)} title={`What ${sourcing?.email ?? "this mailbox"} sends`} description="Choose where this mailbox gets its signature."
178
+ footer={<><Button variant="outline" onClick={() => setSourcing(null)}>Cancel</Button><Button disabled={srcChoice === "template" && srcTemplate === ""} onClick={() => sourcing && setPending({ kind: "source", mailbox: sourcing, source: srcChoice, templateId: srcTemplate })}>Continue</Button></>}>
179
+ <select aria-label="Signature source" className={`${SELECT} w-full`} value={srcChoice} onChange={(e) => setSrcChoice(e.target.value as SourceChoice)}>{SOURCE_CHOICES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}</select>
180
+ {srcChoice === "template" ? <select aria-label="Template" className={`${SELECT} w-full`} value={srcTemplate} onChange={(e) => setSrcTemplate(e.target.value)}><option value="">Choose a template</option>{(templates.data ?? []).map((t) => <option key={t.id} value={t.id}>{templateName(t)}</option>)}</select> : null}
181
+ </FormSheet>
182
+ <ConfirmSpendDialog
183
+ open={pending !== null}
184
+ tier="external"
185
+ title={text?.title ?? ""}
186
+ affected={text?.count}
187
+ description={text?.body}
188
+ confirmLabel={text?.label}
189
+ confirming={run.loading}
190
+ onCancel={() => setPending(null)}
191
+ onConfirm={() => { if (pending) void run.mutate(pending); }}
192
+ />
193
+ </div>
194
+ );
195
+ }
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SignaturePolicy: the organisation-wide signature policy: whether signatures are managed at all, the default template,
5
+ * a template per department, whether employees may change the parts you leave unlocked, which variant cold sends use, and
6
+ * native installation.
7
+ *
8
+ * SDK operations, in order (the policy, template and member reads come from the shared hooks):
9
+ * 1. getSignatureSettings (GET /signatures/settings) read the stored policy and its version; the form shows only once it has loaded
10
+ * 2. updateSignatureSettings (PUT /signatures/settings) write REPLACES the policy and sends the version that was read (409 if someone saved first).
11
+ * It changes what every managed mailbox sends, so ConfirmSpendDialog (external) comes first, sized from the managed mailboxes.
12
+ * The payload is the stored policy's known keys plus the fields the person changed; a field that was never stored is left out so
13
+ * the API applies its own default instead of one invented here. Clearing the default template sends null, clearing a department
14
+ * removes it from the map. Turning management on needs a default template; enforcing needs every active mailbox assigned (the API checks).
15
+ */
16
+
17
+ import { useState } from "react";
18
+ import { Button, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { SectionCard } from "@/g8/components/screen/section-card";
22
+ import { g8Put } from "@/g8/lib/g8-client";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { plural } from "@/g8/lib/plural";
25
+ import { useSignatureDeployments, useSignatureMembers, useSignaturePolicy, useSignatureTemplates } from "../hooks/use-signatures-data";
26
+ import { COLD_VARIANTS, POLICY_MODES, isConflict, isManaged, knownPolicy, templateName } from "../lib/signatures-types";
27
+ import type { PolicyData, PolicyMode, SignaturePolicy as Policy } from "../lib/signatures-types";
28
+
29
+ const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
30
+
31
+ export function SignaturePolicy() {
32
+ const policy = useSignaturePolicy();
33
+ const [saved, setSaved] = useState(false);
34
+ if (policy.error && !policy.data) return <ApiError error={policy.error} title="Could not load the policy" onRetry={() => void policy.refetch()} />;
35
+ if (!policy.data) return <Skeleton className="h-40 w-full" aria-label="Loading the policy" />;
36
+ // A save changes the version, which remounts the form, so the "saved" note lives here.
37
+ return (
38
+ <div className="grid gap-2">
39
+ {saved ? <p role="status" className="text-sm">Policy saved.</p> : null}
40
+ <PolicyForm key={policy.data.version} policy={policy.data} onReload={() => void policy.refetch()} onSaved={() => setSaved(true)} />
41
+ </div>
42
+ );
43
+ }
44
+
45
+ function PolicyForm({ policy, onReload, onSaved }: { policy: Policy; onReload: () => void; onSaved: () => void }) {
46
+ const templates = useSignatureTemplates();
47
+ const members = useSignatureMembers();
48
+ const deployments = useSignatureDeployments();
49
+ const stored = knownPolicy(policy.data);
50
+ const [mode, setMode] = useState<PolicyMode>((stored.mode as PolicyMode) ?? "off");
51
+ const [defaultId, setDefaultId] = useState(stored.default_template_id ?? "");
52
+ const [byDept, setByDept] = useState<Record<string, string>>(stored.department_templates ?? {});
53
+ const [overrides, setOverrides] = useState(stored.allow_employee_overrides ?? true);
54
+ const [cold, setCold] = useState<"full" | "compact">(stored.cold_sends_variant ?? "compact");
55
+ const [native, setNative] = useState(stored.native_enabled ?? false);
56
+ const [confirm, setConfirm] = useState(false);
57
+ const managed = (deployments.data ?? []).filter((d) => isManaged(d)).length;
58
+ const departments = [...new Set([...(members.data ?? []).map((m) => m.department).filter((d): d is string => Boolean(d)), ...Object.keys(stored.department_templates ?? {})])].sort();
59
+ const needsDefault = mode !== "off" && defaultId === "";
60
+
61
+ const next: Partial<PolicyData> = {
62
+ ...stored,
63
+ ...(mode !== stored.mode ? { mode } : {}),
64
+ ...(defaultId !== (stored.default_template_id ?? "") ? { default_template_id: defaultId === "" ? null : defaultId } : {}),
65
+ ...(JSON.stringify(byDept) !== JSON.stringify(stored.department_templates ?? {}) ? { department_templates: byDept } : {}),
66
+ ...(overrides !== (stored.allow_employee_overrides ?? true) ? { allow_employee_overrides: overrides } : {}),
67
+ ...(cold !== (stored.cold_sends_variant ?? "compact") ? { cold_sends_variant: cold } : {}),
68
+ ...(native !== (stored.native_enabled ?? false) ? { native_enabled: native } : {}),
69
+ };
70
+ const changed = JSON.stringify(next) !== JSON.stringify(stored);
71
+
72
+ const save = useG8Mutation(async (): Promise<Policy> => (await g8Put<Policy>("/signatures/settings", { version: policy.version, data: next })).data, {
73
+ onSuccess: () => { setConfirm(false); onSaved(); },
74
+ onError: () => setConfirm(false),
75
+ invalidates: [["signatures", "policy"], ["signatures", "deployments"]],
76
+ });
77
+
78
+ const setDept = (dept: string, id: string): void => {
79
+ const rest = Object.fromEntries(Object.entries(byDept).filter(([k]) => k !== dept));
80
+ setByDept(id === "" ? rest : { ...rest, [dept]: id });
81
+ };
82
+ const options = (templates.data ?? []).map((t) => <option key={t.id} value={t.id}>{templateName(t)}</option>);
83
+
84
+ return (
85
+ <SectionCard title="Signature policy" description={`Applies to ${plural(managed, "managed mailbox", "managed mailboxes")}.`}>
86
+ <form className="grid gap-4" onSubmit={(e) => { e.preventDefault(); if (changed && !needsDefault) setConfirm(true); }}>
87
+ <fieldset className="grid gap-2 text-sm">
88
+ <legend className="mb-1 font-medium">Management</legend>
89
+ {POLICY_MODES.map((m) => (
90
+ <label key={m.value} className="flex items-start gap-2"><input type="radio" name="g8-sig-mode" className="mt-1" checked={mode === m.value} onChange={() => setMode(m.value)} /><span><span className="font-medium">{m.label}</span><span className="text-muted-foreground block text-xs">{m.hint}</span></span></label>
91
+ ))}
92
+ </fieldset>
93
+ <label className="grid gap-1 text-sm">Default template
94
+ <select aria-label="Default template" className={SELECT} value={defaultId} onChange={(e) => setDefaultId(e.target.value)}><option value="">None</option>{options}</select>
95
+ </label>
96
+ {needsDefault ? <p role="alert" className="text-error text-xs">Choose a default template before turning management on.</p> : null}
97
+ {departments.length > 0 ? (
98
+ <fieldset className="grid gap-2">
99
+ <legend className="mb-1 text-sm font-medium">Template by department</legend>
100
+ {departments.map((d) => (
101
+ <label key={d} className="grid grid-cols-[10rem_1fr] items-center gap-2 text-sm">{d}
102
+ <select aria-label={`Template for ${d}`} className={SELECT} value={byDept[d] ?? ""} onChange={(e) => setDept(d, e.target.value)}><option value="">The default template</option>{options}</select>
103
+ </label>
104
+ ))}
105
+ </fieldset>
106
+ ) : null}
107
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={overrides} onChange={(e) => setOverrides(e.target.checked)} />Employees may change the parts of their signature that are not locked</label>
108
+ <label className="grid gap-1 text-sm">Cold sends use
109
+ <select aria-label="Cold sends variant" className={SELECT} value={cold} onChange={(e) => setCold(e.target.value as "full" | "compact")}>{COLD_VARIANTS.map((v) => <option key={v.value} value={v.value}>{v.label}</option>)}</select>
110
+ </label>
111
+ <label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={native} onChange={(e) => setNative(e.target.checked)} />Allow installing signatures inside the mail account (native installation)</label>
112
+ {save.error && isConflict(save.error) ? (
113
+ <div className="grid gap-2"><ApiError error={save.error} title="The policy changed since you opened it" /><div><Button type="button" size="sm" variant="outline" onClick={onReload}>Load the newer policy</Button></div></div>
114
+ ) : save.error ? <ApiError error={save.error} title="Could not save the policy" /> : null}
115
+ <div><Button type="submit" loading={save.loading} disabled={!changed || needsDefault}>Save policy</Button></div>
116
+ </form>
117
+ <ConfirmSpendDialog
118
+ open={confirm}
119
+ tier="external"
120
+ title={`Save the policy for ${plural(managed, "managed mailbox", "managed mailboxes")}?`}
121
+ affected={managed}
122
+ description="The policy decides which signature each managed mailbox sends and whether employees can change it. Turning management off stops managing every one of them."
123
+ confirmLabel="Save policy"
124
+ confirming={save.loading}
125
+ onCancel={() => setConfirm(false)}
126
+ onConfirm={() => void save.mutate(undefined)}
127
+ />
128
+ </SectionCard>
129
+ );
130
+ }
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SafeSignatureFrame and TemplatePreview: show what a signature will look like.
5
+ *
6
+ * SafeSignatureFrame renders signature HTML (which comes from the API, and a template can carry custom markup) inside a
7
+ * sandboxed iframe with no scripts, no same-origin access and a content-security-policy that allows only inline styles and
8
+ * https or data images. The markup is never put into the page's own DOM, so it cannot run code or restyle the app.
9
+ *
10
+ * TemplatePreview renders a template with the signature renderer, read-only (nothing is saved or sent):
11
+ * 1. previewSignatureTemplate (POST /signatures/preview) write* an UNSAVED draft; waits until the draft is valid, debounced
12
+ * 2. previewSavedSignatureTemplate (POST /signatures/templates/{id}/preview) write* a stored template
13
+ * *The contract tiers a preview as a write because it is a POST; it changes nothing. Pick an employee AND a mailbox to render
14
+ * with that person's real details; without both, the API renders sample data. Variants: full, compact (what cold sends use)
15
+ * and with the campaign banner, plus the plain-text version.
16
+ */
17
+
18
+ import { useMemo, useState } from "react";
19
+ import { Button, Skeleton } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { g8Post } from "@/g8/lib/g8-client";
22
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
23
+ import { useDebounced } from "@/g8/lib/use-debounced-value";
24
+ import { useSignatureMailboxes, useSignatureMembers } from "../hooks/use-signatures-data";
25
+ import { memberLabel } from "../lib/signatures-types";
26
+ import type { PreviewVariant, TemplateData, TemplatePreview as TemplatePreviewData } from "../lib/signatures-types";
27
+
28
+ const POLICY = "default-src 'none'; img-src https: data:; style-src 'unsafe-inline'";
29
+ const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
30
+
31
+ /** The document the frame shows: the signature's markup between fixed, trusted wrapper tags. */
32
+ export function frameDocument(html: string): string {
33
+ return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${POLICY}"><style>body{margin:12px;font:14px/1.4 Arial,sans-serif;color:#222;background:#fff}</style></head><body>${html}</body></html>`;
34
+ }
35
+
36
+ export interface SafeSignatureFrameProps {
37
+ html: string;
38
+ title?: string;
39
+ className?: string;
40
+ }
41
+
42
+ export function SafeSignatureFrame({ html, title = "Signature preview", className }: SafeSignatureFrameProps) {
43
+ const doc = useMemo(() => frameDocument(html), [html]);
44
+ // sandbox="" is the strictest setting: no scripts, no forms, no popups, no same-origin. Do not add allow-* tokens.
45
+ return <iframe title={title} sandbox="" referrerPolicy="no-referrer" srcDoc={doc} className={className ?? "border-border h-56 w-full rounded-md border bg-white"} />;
46
+ }
47
+
48
+ const VARIANTS: { value: PreviewVariant; label: string }[] = [
49
+ { value: "full", label: "Full" },
50
+ { value: "compact", label: "Compact (cold sends)" },
51
+ { value: "campaign", label: "With campaign banner" },
52
+ ];
53
+
54
+ export interface TemplatePreviewProps {
55
+ /** An unsaved draft to render. Omit to render the stored template `templateId`. */
56
+ draft?: TemplateData;
57
+ /** Hold a draft back until it passes the template rules (the API refuses an invalid one). */
58
+ draftValid?: boolean;
59
+ templateId?: string;
60
+ className?: string;
61
+ }
62
+
63
+ export function TemplatePreview({ draft, draftValid = true, templateId, className }: TemplatePreviewProps) {
64
+ const members = useSignatureMembers();
65
+ const mailboxes = useSignatureMailboxes();
66
+ const [memberId, setMemberId] = useState("");
67
+ const [mailboxId, setMailboxId] = useState("");
68
+ const [variant, setVariant] = useState<PreviewVariant>("full");
69
+ const [plain, setPlain] = useState(false);
70
+ const real = memberId !== "" && mailboxId !== "";
71
+ const who = real ? { mailbox_id: Number(mailboxId), member_id: memberId } : {};
72
+ const body = JSON.stringify(draft ? { data: draft, ...who } : who);
73
+ const settled = useDebounced(body, draft ? 600 : 0);
74
+ const ready = (draft ? draftValid : Boolean(templateId)) && settled === body;
75
+
76
+ const result = useG8Query(
77
+ (signal) => {
78
+ const payload = JSON.parse(settled) as Record<string, unknown>;
79
+ const path = draft ? "/signatures/preview" : `/signatures/templates/${encodeURIComponent(templateId ?? "")}/preview`;
80
+ return g8Post<TemplatePreviewData>(path, payload, { signal }).then((r) => r.data);
81
+ },
82
+ [settled, templateId, Boolean(draft)],
83
+ { enabled: ready },
84
+ );
85
+ const shown = result.data?.[variant];
86
+
87
+ return (
88
+ <div className={className ?? "grid gap-3"}>
89
+ <div className="grid gap-2 sm:grid-cols-2">
90
+ <label className="grid gap-1 text-xs">
91
+ Show it for
92
+ <select aria-label="Preview for employee" className={SELECT} value={memberId} onChange={(e) => setMemberId(e.target.value)}>
93
+ <option value="">Sample employee</option>
94
+ {(members.data ?? []).map((m) => <option key={m.id} value={m.id}>{memberLabel(m)}</option>)}
95
+ </select>
96
+ </label>
97
+ <label className="grid gap-1 text-xs">
98
+ From mailbox
99
+ <select aria-label="Preview from mailbox" className={SELECT} value={mailboxId} onChange={(e) => setMailboxId(e.target.value)}>
100
+ <option value="">Sample mailbox</option>
101
+ {mailboxes.mailboxes.map((m) => <option key={m.id} value={m.id}>{m.email}</option>)}
102
+ </select>
103
+ </label>
104
+ </div>
105
+ {(memberId === "") !== (mailboxId === "") ? <p role="note" className="text-muted-foreground text-xs">Pick both an employee and a mailbox to preview with real details; until then the sample is shown.</p> : null}
106
+ <div className="flex flex-wrap items-center gap-1" role="group" aria-label="Preview variant">
107
+ {VARIANTS.map((v) => <Button key={v.value} type="button" size="sm" variant={variant === v.value ? "default" : "outline"} aria-pressed={variant === v.value} onClick={() => setVariant(v.value)}>{v.label}</Button>)}
108
+ <Button type="button" size="sm" variant={plain ? "default" : "outline"} aria-pressed={plain} onClick={() => setPlain(!plain)}>Plain text</Button>
109
+ </div>
110
+ {!ready && draft && !draftValid ? <p role="note" className="text-muted-foreground text-sm">The preview appears once the template is valid.</p> : result.error ? (
111
+ <ApiError error={result.error} title="Could not render the preview" onRetry={() => void result.refetch()} />
112
+ ) : result.loading && !result.data ? (
113
+ <Skeleton className="h-56 w-full" aria-label="Rendering the preview" />
114
+ ) : shown ? (
115
+ plain ? <pre aria-label="Plain text signature" className="bg-muted max-h-56 overflow-auto whitespace-pre-wrap rounded-md border p-3 text-sm">{shown.plain}</pre> : <SafeSignatureFrame html={shown.html} />
116
+ ) : null}
117
+ </div>
118
+ );
119
+ }
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ /**
4
+ * SignatureReport: signature and campaign engagement: how many impressions and clicks each mailbox and each campaign got.
5
+ *
6
+ * SDK operations:
7
+ * 1. getSignatureReport (GET /signatures/reports) read counts by event kind, mailbox, campaign and source, plus the limits of the numbers
8
+ * Mailbox and campaign names come from the shared hooks. The numbers are only what the API can see: it says when click
9
+ * tracking is not configured, and lists its own limitations (for example that a natively installed signature reports no
10
+ * impressions); the screen shows them instead of presenting a zero as a measurement. Events are kept for the number of days
11
+ * the report states.
12
+ */
13
+
14
+ import { Skeleton } from "@graph8/sdk/ui";
15
+ import { ApiError } from "@/g8/components/api-error";
16
+ import { EmptyState } from "@/g8/components/empty-state";
17
+ import { SectionCard } from "@/g8/components/screen/section-card";
18
+ import { StatStrip } from "@/g8/components/screen/stat-strip";
19
+ import { humanizeStatus as humanize } 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 { useSignatureCampaigns, useSignatureMailboxes } from "../hooks/use-signatures-data";
23
+ import type { ReportMetric, SignatureReport as Report } from "../lib/signatures-types";
24
+
25
+ function totals(metrics: ReportMetric[], by: (m: ReportMetric) => string): [string, Map<string, number>][] {
26
+ const out = new Map<string, Map<string, number>>();
27
+ for (const m of metrics) {
28
+ const key = by(m);
29
+ const row = out.get(key) ?? new Map<string, number>();
30
+ row.set(m.kind, (row.get(m.kind) ?? 0) + m.count);
31
+ out.set(key, row);
32
+ }
33
+ return [...out].sort((a, b) => a[0].localeCompare(b[0]));
34
+ }
35
+
36
+ export function SignatureReport() {
37
+ const report = useG8Query((signal) => g8Get<Report>("/signatures/reports", {}, { signal }).then((r) => r.data), [], { key: ["signatures", "report"] });
38
+ const box = useSignatureMailboxes();
39
+ const campaigns = useSignatureCampaigns();
40
+ if (report.error && !report.data) return <ApiError error={report.error} title="Could not load the report" onRetry={() => void report.refetch()} />;
41
+ if (!report.data) return <Skeleton className="h-40 w-full" aria-label="Loading the report" />;
42
+
43
+ const r = report.data;
44
+ const metrics = r.metrics ?? [];
45
+ const kinds = [...new Set(metrics.map((m) => m.kind))].sort();
46
+ const email = (id: number | null | undefined): string => (id == null ? "No mailbox" : box.mailboxes.find((m) => m.id === id)?.email || "A mailbox that is no longer listed");
47
+ const campaign = (id: string | null | undefined): string => (id ? campaigns.data?.find((c) => c.id === id)?.data.name || "A campaign that is no longer listed" : "No campaign");
48
+ const byMailbox = totals(metrics, (m) => email(m.mailbox_id));
49
+ const byCampaign = totals(metrics.filter((m) => m.campaign_id), (m) => campaign(m.campaign_id));
50
+ const grid = (rows: [string, Map<string, number>][], first: string) => (
51
+ <table className="w-full text-sm">
52
+ <thead><tr className="text-muted-foreground text-left text-xs"><th className="py-1 font-normal">{first}</th>{kinds.map((k) => <th key={k} className="py-1 text-right font-normal">{humanize(k)}</th>)}</tr></thead>
53
+ <tbody>{rows.map(([name, counts]) => <tr key={name} className="border-t"><td className="py-1.5">{name}</td>{kinds.map((k) => <td key={k} className="py-1.5 text-right tabular-nums">{(counts.get(k) ?? 0).toLocaleString()}</td>)}</tr>)}</tbody>
54
+ </table>
55
+ );
56
+
57
+ return (
58
+ <div className="grid gap-4">
59
+ {!r.tracking_available ? <p role="note" className="rounded-md border p-3 text-sm">Click tracking is not set up for this workspace, so clicks are not counted. Impressions that the API can see still are.</p> : null}
60
+ {metrics.length === 0 ? (
61
+ <EmptyState title="No signature activity yet" description={`Impressions and clicks appear once managed signatures go out. Events are kept for ${r.retention_days ?? "a limited number of"} days.`} />
62
+ ) : (
63
+ <>
64
+ <StatStrip items={kinds.map((k) => ({ label: humanize(k), value: metrics.filter((m) => m.kind === k).reduce((n, m) => n + m.count, 0).toLocaleString() }))} />
65
+ <SectionCard title="By mailbox">{grid(byMailbox, "Mailbox")}</SectionCard>
66
+ {byCampaign.length > 0 ? <SectionCard title="By campaign">{grid(byCampaign, "Campaign")}</SectionCard> : null}
67
+ </>
68
+ )}
69
+ {(r.limitations ?? []).length > 0 ? (
70
+ <SectionCard title="How to read these numbers">
71
+ <ul className="text-muted-foreground list-disc space-y-1 pl-5 text-sm">{(r.limitations ?? []).map((l) => <li key={l}>{l}</li>)}</ul>
72
+ {r.retention_days ? <p className="text-muted-foreground mt-2 text-xs">Events are kept for {r.retention_days} days.</p> : null}
73
+ </SectionCard>
74
+ ) : null}
75
+ </div>
76
+ );
77
+ }