@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,106 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ProgrammaticSource: the rows the pages are made from: what the source is, its columns and first rows, switching it, and
5
+ * importing a sitemap as the source.
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getProgrammaticWorkspace (GET /programmatic-campaigns/{id}/workspace) read source, columns, sample rows, row count
9
+ * 2. updateProgrammaticWorkspace (PATCH /programmatic-campaigns/{id}/workspace) write switch to another list or table (works at any status)
10
+ * 3. importProgrammaticSitemap (POST /programmatic-campaigns/{id}/imports/sitemap) write REPLACES the source with a table of the sitemap's URLs
11
+ * A source change marks the pages as changed; render again to apply it. The sitemap import fetches the sitemap (and, if asked,
12
+ * each page) from the public web; the maximum is left to the API's default unless a number is typed.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
18
+ import { EmptyValue, KeyValueList } from "@/g8/components/screen/key-value-list";
19
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
20
+ import { g8Patch, g8Post } from "@/g8/lib/g8-client";
21
+ import { plural } from "@/g8/lib/plural";
22
+ import { text } from "@/g8/pages/lib/studio";
23
+ import { useProgrammaticWorkspace } from "../hooks/use-programmatic-workspace";
24
+ import { SOURCE_TYPES, sourceComplete, sourceOf } from "../lib/programmatic-types";
25
+ import type { Campaign, SourceType } from "../lib/programmatic-types";
26
+ import { ProgrammaticSourceFields } from "./programmatic-source-fields";
27
+ import type { SourceValue } from "./programmatic-source-fields";
28
+
29
+ export interface ProgrammaticSourceProps {
30
+ campaign: Campaign;
31
+ }
32
+
33
+ export function ProgrammaticSource({ campaign }: ProgrammaticSourceProps) {
34
+ const ws = useProgrammaticWorkspace(campaign.id);
35
+ if (ws.error && !ws.data) return <ApiError error={ws.error} title="Could not load the source" onRetry={() => void ws.refetch()} />;
36
+ if (!ws.data) return <Skeleton className="h-32 w-full" aria-busy="true" />;
37
+ const src = sourceOf({ data_source: ws.data.data?.source, data_source_type: campaign.data_source_type, list_id: campaign.list_id });
38
+ const cols = (ws.data.data?.columns ?? []).slice(0, 6);
39
+ const rows = ws.data.data?.sample_rows ?? [];
40
+ const total = ws.data.data?.total_rows ?? null;
41
+ return (
42
+ <div className="grid gap-4">
43
+ <KeyValueList columns={2} items={[
44
+ { label: "Source", value: SOURCE_TYPES.find((t) => t.value === src.type)?.label ?? null },
45
+ { label: "Name", value: src.name || null },
46
+ { label: "Rows", value: total == null ? null : total.toLocaleString() },
47
+ { label: "Columns", value: (ws.data.data?.columns ?? []).length ? (ws.data.data?.columns ?? []).join(", ") : null, wide: true },
48
+ ]} />
49
+ {rows.length > 0 ? (
50
+ <div className="overflow-x-auto rounded-md border">
51
+ <table aria-label="First source rows" className="w-full text-sm">
52
+ <thead className="text-muted-foreground text-left text-xs"><tr>{cols.map((c) => <th key={c} className="p-2">{c}</th>)}</tr></thead>
53
+ <tbody>{rows.map((r, i) => <tr key={i} className="border-t">{cols.map((c) => <td key={c} className="max-w-48 truncate p-2">{text(r[c]) || <EmptyValue />}</td>)}</tr>)}</tbody>
54
+ </table>
55
+ </div>
56
+ ) : <p className="text-muted-foreground text-sm">The source has no rows to show.</p>}
57
+ <SwitchSource campaign={campaign} current={src} />
58
+ <ImportSitemap campaign={campaign} rows={total} />
59
+ </div>
60
+ );
61
+ }
62
+
63
+ function SwitchSource({ campaign, current }: { campaign: Campaign; current: { type: string; id: string; name: string } }) {
64
+ const [v, setV] = useState<SourceValue>({ type: (SOURCE_TYPES.find((t) => t.value === current.type)?.value ?? "list") as SourceType, listId: current.type === "list" && current.id ? Number(current.id) : null, tableId: current.type !== "list" ? current.id : "", tableName: current.name });
65
+ const same = v.type === current.type && (v.type === "list" ? String(v.listId ?? "") === current.id : v.tableId === current.id);
66
+ const save = useG8Mutation(() => g8Patch(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}/workspace`, v.type === "list" ? { data_source_type: v.type, list_id: v.listId } : { data_source_type: v.type, data_source_id: v.tableId, ...(v.tableName ? { data_source_name: v.tableName } : {}) }), { invalidates: [["programmatic"]] });
67
+ return (
68
+ <form className="grid gap-3 rounded-md border p-3" onSubmit={(e) => { e.preventDefault(); if (!same && sourceComplete({ source_type: v.type, list_id: v.listId, table_id: v.tableId })) void save.mutate(undefined); }}>
69
+ <h3 className="text-sm font-medium">Use a different source</h3>
70
+ <ProgrammaticSourceFields value={v} onChange={setV} currentName={current.name} idPrefix="g8-pp-switch" />
71
+ {save.error ? <ApiError error={save.error} title="Could not change the source" /> : null}
72
+ {save.data ? <p role="status" className="text-xs">Source changed. Render the pages again to apply it.</p> : null}
73
+ <div><Button type="submit" size="sm" loading={save.loading} disabled={same || !sourceComplete({ source_type: v.type, list_id: v.listId, table_id: v.tableId })}>Change source</Button></div>
74
+ </form>
75
+ );
76
+ }
77
+
78
+ function ImportSitemap({ campaign, rows }: { campaign: Campaign; rows: number | null }) {
79
+ const [url, setUrl] = useState("");
80
+ const [name, setName] = useState("");
81
+ const [max, setMax] = useState("");
82
+ const [fetchPages, setFetchPages] = useState(false);
83
+ const maxOk = max.trim() === "" || (/^\d+$/.test(max.trim()) && Number(max) >= 1);
84
+ const valid = /^https?:\/\/\S+$/i.test(url.trim()) && maxOk;
85
+ const run = useG8Mutation(() => {
86
+ const body: Record<string, unknown> = { sitemap_url: url.trim(), fetch_pages: fetchPages };
87
+ if (name.trim()) body.source_name = name.trim();
88
+ if (max.trim()) body.max_urls = Number(max);
89
+ return g8Post<{ rows_imported?: number }>(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}/imports/sitemap`, body);
90
+ }, { invalidates: [["programmatic"]] });
91
+ return (
92
+ <form className="grid gap-3 rounded-md border p-3" onSubmit={(e) => { e.preventDefault(); if (valid) void run.mutate(undefined); }}>
93
+ <h3 className="text-sm font-medium">Import a sitemap as the source</h3>
94
+ <p role="note" className="text-xs">This replaces the current source{rows == null ? "" : ` (${plural(rows, "row")})`} with a new table of the sitemap&apos;s addresses. The current table is not deleted; switch back to it above.</p>
95
+ <div className="grid gap-3 sm:grid-cols-2">
96
+ <div><Label htmlFor="g8-pp-sitemap" className="mb-1">Sitemap address</Label><Input id="g8-pp-sitemap" type="url" placeholder="https://example.com/sitemap.xml" value={url} onChange={(e) => setUrl(e.target.value)} /></div>
97
+ <div><Label htmlFor="g8-pp-sitemap-name" className="mb-1">Table name (optional)</Label><Input id="g8-pp-sitemap-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
98
+ <div><Label htmlFor="g8-pp-sitemap-max" className="mb-1">Most addresses (optional)</Label><Input id="g8-pp-sitemap-max" inputMode="numeric" value={max} onChange={(e) => setMax(e.target.value)} aria-invalid={!maxOk} /></div>
99
+ <label className="flex items-center gap-2 self-end text-sm"><input type="checkbox" checked={fetchPages} onChange={(e) => setFetchPages(e.target.checked)} />Also read each page</label>
100
+ </div>
101
+ {run.error ? <ApiError error={run.error} title="Could not import the sitemap" /> : null}
102
+ {run.data ? <p role="status" className="text-xs">Imported {run.data.data.rows_imported == null ? "the sitemap" : plural(run.data.data.rows_imported, "address", "addresses")}. It is now the source.</p> : null}
103
+ <div><Button type="submit" size="sm" variant="outline" loading={run.loading} disabled={!valid}>Replace source with sitemap</Button></div>
104
+ </form>
105
+ );
106
+ }
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ProgrammaticVariables: the {{variables}} of the template and slug, whether the source has a column for each, and the
5
+ * per-variable rules the renderer applies (a fallback for empty values, a character limit and how to cut).
6
+ *
7
+ * SDK operations, in order:
8
+ * 1. getProgrammaticCampaignVariables (GET /programmatic-campaigns/{id}/variables) read variables, matches, sample columns
9
+ * 2. updateProgrammaticWorkspace (PATCH /programmatic-campaigns/{id}/workspace) write the whole variable_config
10
+ * The API replaces the whole array, so the save merges the edited variables into the stored entries: every other entry, and every
11
+ * key the form does not show, is sent back unchanged. A cleared fallback or limit is sent as null. Only variables whose values
12
+ * changed are written. Saving marks the pages as changed; render again to apply it.
13
+ */
14
+
15
+ import { useState } from "react";
16
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
17
+ import { ApiError } from "@/g8/components/api-error";
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 { g8Get, g8Patch } from "@/g8/lib/g8-client";
22
+ import type { Rec } from "@/g8/pages/lib/studio";
23
+ import { changedEdits, editOf, mergeVariableConfig, validMaxChars } from "../lib/programmatic-types";
24
+ import type { Campaign, VariableEdit, VariableStatus } from "../lib/programmatic-types";
25
+
26
+ export interface ProgrammaticVariablesProps {
27
+ campaign: Campaign;
28
+ }
29
+
30
+ interface VariablesPayload { variables?: VariableStatus[]; sample_fields?: string[]; slug_template?: string }
31
+
32
+ export function ProgrammaticVariables({ campaign }: ProgrammaticVariablesProps) {
33
+ const q = useG8Query(
34
+ (signal) => g8Get<VariablesPayload>(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}/variables`, {}, { signal }).then((r) => r.data),
35
+ [campaign.id], { key: ["programmatic", "variables", campaign.id] },
36
+ );
37
+ const stored = useG8Query(
38
+ (signal) => g8Get<Campaign>(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}`, {}, { signal }).then((r) => r.data),
39
+ [campaign.id], { key: ["programmatic", "detail", campaign.id] },
40
+ );
41
+ if (q.error && !q.data) return <ApiError error={q.error} title="Could not load the variables" onRetry={() => void q.refetch()} />;
42
+ if (stored.error && !stored.data) return <ApiError error={stored.error} title="Could not load the campaign" onRetry={() => void stored.refetch()} />;
43
+ if (!q.data || !stored.data) return <Skeleton className="h-32 w-full" aria-busy="true" />;
44
+ return <Rules key={campaign.id} campaignId={campaign.id} vars={q.data.variables ?? []} columns={q.data.sample_fields ?? []} existing={stored.data.variable_config ?? []} />;
45
+ }
46
+
47
+ function Rules({ campaignId, vars, columns, existing }: { campaignId: string; vars: VariableStatus[]; columns: string[]; existing: Rec[] }) {
48
+ const [edits, setEdits] = useState<Record<string, VariableEdit>>(() => Object.fromEntries(vars.map((v) => [v.name, editOf(v)])));
49
+ const changed = changedEdits(vars, edits);
50
+ const valid = changed.every((e) => validMaxChars(e.maxChars));
51
+ const set = (name: string, patch: Partial<VariableEdit>): void => setEdits((cur) => ({ ...cur, [name]: { ...cur[name], ...patch } }));
52
+ const save = useG8Mutation(() => g8Patch(`/programmatic-campaigns/${encodeURIComponent(campaignId)}/workspace`, { variable_config: mergeVariableConfig(existing, changed) }), {
53
+ invalidates: [["programmatic"]],
54
+ });
55
+ if (vars.length === 0) return <p className="text-muted-foreground text-sm">The template and the address pattern use no {"{{variables}}"}, so every page is the same.</p>;
56
+ return (
57
+ <form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (valid && changed.length) void save.mutate(undefined); }}>
58
+ <p className="text-muted-foreground text-xs">Source columns: {columns.length ? columns.join(", ") : "none found"}. A variable with no matching column renders empty unless it has a fallback.</p>
59
+ <div className="overflow-x-auto rounded-md border">
60
+ <table className="w-full text-sm">
61
+ <thead className="text-muted-foreground text-left text-xs"><tr><th className="p-2">Variable</th><th className="p-2">Source column</th><th className="p-2">Fallback</th><th className="p-2">Character limit</th><th className="p-2">When too long</th></tr></thead>
62
+ <tbody>
63
+ {vars.map((v) => {
64
+ const e = edits[v.name] ?? editOf(v);
65
+ return (
66
+ <tr key={v.name} className="border-t">
67
+ <td className="p-2 font-mono text-xs">{`{{${v.name}}}`}{v.in_slug ? <span className="text-muted-foreground"> (address)</span> : null}</td>
68
+ <td className="p-2"><StatusPill tone={v.matched_in_source ? "positive" : "warning"}>{v.matched_in_source ? "Found" : "No column"}</StatusPill></td>
69
+ <td className="p-2"><Input aria-label={`Fallback for ${v.name}`} value={e.fallback} onChange={(ev) => set(v.name, { fallback: ev.target.value })} /></td>
70
+ <td className="p-2"><Input aria-label={`Character limit for ${v.name}`} inputMode="numeric" className="w-24" value={e.maxChars} onChange={(ev) => set(v.name, { maxChars: ev.target.value })} aria-invalid={!validMaxChars(e.maxChars)} /></td>
71
+ <td className="p-2"><select aria-label={`Cut ${v.name} with`} className="border-input bg-background h-9 rounded-md border px-2 text-sm" value={e.policy} onChange={(ev) => set(v.name, { policy: ev.target.value as "ellipsis" | "clip" })}><option value="ellipsis">Add ...</option><option value="clip">Cut off</option></select></td>
72
+ </tr>
73
+ );
74
+ })}
75
+ </tbody>
76
+ </table>
77
+ </div>
78
+ {!valid ? <p role="alert" className="text-error text-xs">A character limit is a whole number from 1 to 5000.</p> : null}
79
+ {save.error ? <ApiError error={save.error} title="Could not save the variables" /> : null}
80
+ {save.data ? <p role="status" className="text-xs">Saved. Render the pages again to apply the new rules.</p> : null}
81
+ <div><Button type="submit" loading={save.loading} disabled={!valid || changed.length === 0}>Save variable rules</Button></div>
82
+ </form>
83
+ );
84
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ /**
4
+ * useProgrammaticWorkspace: the workspace of one campaign (source, readiness, numbers, links, deployment), shared by every tab
5
+ * through one cache entry so a save on one tab refreshes the others.
6
+ *
7
+ * SDK operations: getProgrammaticWorkspace (GET /programmatic-campaigns/{id}/workspace) read
8
+ */
9
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
10
+ import { g8Get } from "@/g8/lib/g8-client";
11
+ import type { Workspace } from "../lib/programmatic-types";
12
+
13
+ export function useProgrammaticWorkspace(campaignId: string) {
14
+ return useG8Query(
15
+ (signal) => g8Get<Workspace>(`/programmatic-campaigns/${encodeURIComponent(campaignId)}/workspace`, {}, { signal }).then((r) => r.data),
16
+ [campaignId],
17
+ { key: ["programmatic", "workspace", campaignId] },
18
+ );
19
+ }
@@ -0,0 +1,330 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Types, vocabularies and pure helpers for the programmatic-pages block.
5
+ *
6
+ * A programmatic campaign is ONE template landing page plus ONE data source: the API renders one page per source row
7
+ * (a page per account, per city, per integration). Every vocabulary below is copied from the contract
8
+ * (sdk/js/src/generated/schemas.ts and the app's router), not from memory: the API refuses anything else.
9
+ * Responses are open objects (`StudioContentPayload`), so every field is optional and read defensively.
10
+ * Calls nothing.
11
+ */
12
+
13
+ import type { Rec } from "@/g8/pages/lib/studio";
14
+
15
+ export type SourceType = "list" | "workbench" | "csv" | "seo_import";
16
+ export const SOURCE_TYPES: { value: SourceType; label: string; hint: string }[] = [
17
+ { value: "list", label: "A contact list", hint: "One page per person on the list." },
18
+ { value: "workbench", label: "A Workbench table", hint: "One page per row of a table you built in Workbench." },
19
+ { value: "csv", label: "An imported CSV table", hint: "One page per row of a table made from a CSV file." },
20
+ { value: "seo_import", label: "A sitemap import table", hint: "One page per URL imported from a sitemap." },
21
+ ];
22
+
23
+ /** Campaign `status`: where the render run is. */
24
+ export const CAMPAIGN_STATUSES = ["draft", "validating", "generating", "completed", "failed", "paused"] as const;
25
+ /** Campaign and page `lifecycle_state`. */
26
+ export const LIFECYCLE_STATES = ["draft", "ready", "published", "needs_attention", "archived"] as const;
27
+ /** Page `status`: pending, generated, published, failed. */
28
+ export const PAGE_STATUSES = ["pending", "generated", "published", "failed"] as const;
29
+ export const CHANGE_STATES = ["no_changes", "template_changed", "data_changed", "manual_override", "generation_failed", "deploy_failed"] as const;
30
+
31
+ export const REGENERATION_MODES: { value: string; label: string }[] = [
32
+ { value: "manual", label: "Only when I start it" },
33
+ { value: "hourly", label: "Every hour" },
34
+ { value: "daily", label: "Every day" },
35
+ { value: "weekly", label: "Every week" },
36
+ { value: "on_workbench_change", label: "When the Workbench table changes" },
37
+ ];
38
+ export const APPROVAL_MODES: { value: string; label: string; hint: string }[] = [
39
+ { value: "draft_only", label: "Draft only", hint: "Generated pages wait for you." },
40
+ { value: "require_review", label: "Require review", hint: "Generated pages wait for a review." },
41
+ { value: "auto_publish", label: "Publish automatically", hint: "Generated pages are marked live as soon as they render." },
42
+ ];
43
+
44
+ /** One generated campaign (the app's `_campaign_to_dict`). */
45
+ export interface Campaign {
46
+ id: string;
47
+ name: string;
48
+ template_page_id?: string | null;
49
+ list_id?: number | null;
50
+ data_source_type?: string | null;
51
+ data_source?: { type?: string | null; id?: string | number | null; name?: string | null } | null;
52
+ slug_template?: string | null;
53
+ variable_config?: Rec[] | null;
54
+ status?: string | null;
55
+ lifecycle_state?: string | null;
56
+ change_state?: string | null;
57
+ total_pages?: number | null;
58
+ pages_generated?: number | null;
59
+ pages_failed?: number | null;
60
+ error_message?: string | null;
61
+ deployment_run_id?: string | null;
62
+ settings?: Rec | null;
63
+ regeneration?: { mode?: string; approval_mode?: string; changed_rows_only?: boolean } | null;
64
+ last_generated_at?: string | null;
65
+ last_deployed_at?: string | null;
66
+ created_at?: string | null;
67
+ updated_at?: string | null;
68
+ }
69
+
70
+ /** One generated page (the app's `_page_to_dict`). */
71
+ export interface GeneratedPage {
72
+ id: string;
73
+ contact_id?: number | null;
74
+ slug?: string | null;
75
+ status?: string | null;
76
+ lifecycle_state?: string | null;
77
+ change_state?: string | null;
78
+ published_url?: string | null;
79
+ error_message?: string | null;
80
+ manual_override?: Rec | null;
81
+ seo_health?: { score?: number | null; issues?: string[] | null; title?: string | null; word_count?: number | null } | null;
82
+ last_generated_at?: string | null;
83
+ last_deployed_at?: string | null;
84
+ created_at?: string | null;
85
+ }
86
+
87
+ export interface VariableStatus {
88
+ name: string;
89
+ in_template?: boolean;
90
+ in_slug?: boolean;
91
+ matched_in_source?: boolean;
92
+ config?: Rec | null;
93
+ }
94
+
95
+ /** Generation progress (`GET /status`). */
96
+ export interface GenerationStatus {
97
+ status?: string | null;
98
+ total_pages?: number | null;
99
+ pages_generated?: number | null;
100
+ pages_failed?: number | null;
101
+ progress_pct?: number | null;
102
+ error_message?: string | null;
103
+ }
104
+
105
+ export const wording = (value: string | null | undefined): string => (value ? value.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase()) : "");
106
+
107
+ /** The campaigns of a list payload: the app's named key `campaigns`, mirrored onto `items`. */
108
+ export function campaignRows(data: unknown): Campaign[] {
109
+ const rec = (data && typeof data === "object" ? data : {}) as Rec;
110
+ const rows = Array.isArray(data) ? data : Array.isArray(rec.campaigns) ? rec.campaigns : Array.isArray(rec.items) ? rec.items : [];
111
+ return rows as Campaign[];
112
+ }
113
+
114
+ /** Where a campaign draws its rows from: the source record when the API sent one, else the list id. Never invents a default. */
115
+ export function sourceOf(c: Pick<Campaign, "data_source" | "data_source_type" | "list_id">): { type: string; id: string; name: string } {
116
+ const type = c.data_source?.type || c.data_source_type || "";
117
+ const id = c.data_source?.id ?? (type === "list" ? c.list_id : null);
118
+ return { type: type || "", id: id == null || id === "" ? "" : String(id), name: c.data_source?.name || "" };
119
+ }
120
+
121
+ /** A campaign can only be edited (name, slug, source) while it is a draft or failed; the API answers 400 otherwise. */
122
+ export function canEditCampaign(c: Pick<Campaign, "status">): boolean {
123
+ return c.status === "draft" || c.status === "failed";
124
+ }
125
+
126
+ export const isGenerating = (c: Pick<Campaign, "status">): boolean => c.status === "generating";
127
+
128
+ /** The fields of a campaign form; `null` means the person cleared it. */
129
+ export interface CampaignDraft {
130
+ name: string;
131
+ slug_template: string;
132
+ source_type: SourceType;
133
+ list_id: number | null;
134
+ table_id: string;
135
+ table_name: string;
136
+ }
137
+
138
+ /** The PATCH body for an edit: only what changed, so saving can never blank a field the form did not load. */
139
+ export function campaignDiff(current: Campaign, draft: CampaignDraft): Rec {
140
+ const out: Rec = {};
141
+ if (draft.name.trim() !== (current.name ?? "")) out.name = draft.name.trim();
142
+ if (draft.slug_template.trim() !== (current.slug_template ?? "")) out.slug_template = draft.slug_template.trim();
143
+ const was = sourceOf(current);
144
+ const nextId = draft.source_type === "list" ? (draft.list_id == null ? "" : String(draft.list_id)) : draft.table_id;
145
+ if (draft.source_type !== was.type || nextId !== was.id) {
146
+ out.data_source_type = draft.source_type;
147
+ if (draft.source_type === "list") out.list_id = draft.list_id;
148
+ else {
149
+ out.data_source_id = draft.table_id;
150
+ if (draft.table_name) out.data_source_name = draft.table_name;
151
+ }
152
+ }
153
+ return out;
154
+ }
155
+
156
+ /** Whether the source part of a draft is complete (the API answers 400 "require a list_id / data_source_id" otherwise). */
157
+ export function sourceComplete(draft: Pick<CampaignDraft, "source_type" | "list_id" | "table_id">): boolean {
158
+ return draft.source_type === "list" ? draft.list_id != null && draft.list_id > 0 : draft.table_id.trim() !== "";
159
+ }
160
+
161
+ // ── page actions ────────────────────────────────────────────────────────────────────────────────────────────────
162
+
163
+ export type PageActionName = "mark_ready_to_deploy" | "unpublish" | "mark_for_regeneration" | "override" | "reset_override";
164
+ export type BulkActionName = "mark_ready_to_deploy_selected" | "unpublish_selected" | "mark_for_regeneration_selected" | "delete_selected" | "export_urls" | "queue_search_console";
165
+ export type DeployActionName = "deploy_pending_changes" | "republish_all" | "rollback_publish";
166
+
167
+ export const isLive = (p: Pick<GeneratedPage, "status" | "published_url">): boolean => p.status === "published" || Boolean(p.published_url);
168
+
169
+ /** Why a row action is not available for this page, or "" when it is. The API answers 400 for the first one. */
170
+ export function pageActionBlock(page: GeneratedPage, action: PageActionName): string {
171
+ if (action === "mark_ready_to_deploy" && (page.status === "failed" || page.status === "pending")) return "This page has no rendered HTML yet.";
172
+ if (action === "mark_ready_to_deploy" && isLive(page)) return "This page is already live.";
173
+ if (action === "unpublish" && !isLive(page)) return "This page is not live.";
174
+ if (action === "reset_override" && !(page.manual_override && Object.keys(page.manual_override).length) && page.change_state !== "manual_override") return "This page has no manual edit to undo.";
175
+ if (action === "override" && page.status === "failed") return "A failed page has no HTML to replace. Regenerate it first.";
176
+ return "";
177
+ }
178
+
179
+ /**
180
+ * The page ids a bulk action really applies to. The confirm dialog counts this array and the request sends this same array,
181
+ * so what the person agreed to is exactly what is sent. Live pages only for the actions that touch the live site.
182
+ */
183
+ export function bulkTargets(pages: GeneratedPage[], selected: ReadonlySet<string>, action: BulkActionName): string[] {
184
+ const chosen = pages.filter((p) => selected.has(p.id));
185
+ switch (action) {
186
+ case "unpublish_selected": return chosen.filter(isLive).map((p) => p.id);
187
+ case "mark_ready_to_deploy_selected": return chosen.filter((p) => p.status === "generated" && !isLive(p)).map((p) => p.id);
188
+ case "queue_search_console":
189
+ case "export_urls": return chosen.filter((p) => Boolean(p.published_url)).map((p) => p.id);
190
+ default: return chosen.map((p) => p.id);
191
+ }
192
+ }
193
+
194
+ // ── variables ───────────────────────────────────────────────────────────────────────────────────────────────────
195
+
196
+ export interface VariableEdit {
197
+ name: string;
198
+ fallback: string;
199
+ maxChars: string;
200
+ policy: "ellipsis" | "clip";
201
+ }
202
+
203
+ /** What the renderer reads from one `variable_config` entry. */
204
+ export function editOf(v: VariableStatus): VariableEdit {
205
+ const cfg = (v.config ?? {}) as Rec;
206
+ const max = cfg.max_chars;
207
+ return {
208
+ name: v.name,
209
+ fallback: typeof cfg.fallback_value === "string" ? cfg.fallback_value : "",
210
+ maxChars: typeof max === "number" ? String(max) : "",
211
+ policy: cfg.truncation_policy === "clip" ? "clip" : "ellipsis",
212
+ };
213
+ }
214
+
215
+ export function validMaxChars(value: string): boolean {
216
+ return value.trim() === "" || (/^\d+$/.test(value.trim()) && Number(value) >= 1 && Number(value) <= 5000);
217
+ }
218
+
219
+ /**
220
+ * The new `variable_config` array. The API REPLACES the whole array, so every entry the form did not touch is kept as it was
221
+ * (with every key it carries), and a cleared field is sent as null rather than dropped.
222
+ */
223
+ export function mergeVariableConfig(existing: Rec[], edits: VariableEdit[]): Rec[] {
224
+ const out = existing.map((e) => ({ ...e }));
225
+ for (const edit of edits) {
226
+ const at = out.findIndex((e) => String(e.field ?? "").toLowerCase() === edit.name.toLowerCase());
227
+ const base: Rec = at >= 0 ? out[at] : { field: edit.name };
228
+ const next: Rec = { ...base, fallback_value: edit.fallback.trim() === "" ? null : edit.fallback, max_chars: edit.maxChars.trim() === "" ? null : Number(edit.maxChars), truncation_policy: edit.policy };
229
+ if (at >= 0) out[at] = next;
230
+ else out.push(next);
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /** The edits whose values differ from the stored config, so an untouched variable adds no entry. */
236
+ export function changedEdits(vars: VariableStatus[], edits: Record<string, VariableEdit>): VariableEdit[] {
237
+ return vars.flatMap((v) => {
238
+ const now = edits[v.name];
239
+ const was = editOf(v);
240
+ return now && (now.fallback !== was.fallback || now.maxChars !== was.maxChars || now.policy !== was.policy) ? [now] : [];
241
+ });
242
+ }
243
+
244
+ // ── workspace settings ─────────────────────────────────────────────────────────────────────────────────────────
245
+
246
+ export interface SettingsDraft {
247
+ mode: string;
248
+ approval: string;
249
+ changedOnly: boolean;
250
+ basePath: string;
251
+ domain: string;
252
+ thinWords: string;
253
+ schemaRequired: boolean;
254
+ canonical: string;
255
+ sitemap: boolean;
256
+ breadcrumbs: boolean;
257
+ related: boolean;
258
+ }
259
+
260
+ const sub = (settings: Rec | null | undefined, key: string): Rec => ((settings?.[key] && typeof settings[key] === "object" ? settings[key] : {}) as Rec);
261
+
262
+ export function settingsDraftOf(c: Campaign): SettingsDraft {
263
+ const s = c.settings ?? {};
264
+ const regen = { ...sub(s, "regeneration"), ...(c.regeneration ?? {}) } as Rec;
265
+ const dep = sub(s, "deployment");
266
+ const seo = sub(s, "seo");
267
+ const links = sub(s, "links");
268
+ const str = (v: unknown): string => (typeof v === "string" ? v : "");
269
+ return {
270
+ mode: str(regen.mode), approval: str(regen.approval_mode), changedOnly: regen.changed_rows_only === undefined ? true : Boolean(regen.changed_rows_only),
271
+ basePath: str(dep.base_path), domain: str(dep.domain), thinWords: typeof seo.thin_content_words === "number" ? String(seo.thin_content_words) : "",
272
+ schemaRequired: Boolean(seo.schema_required), canonical: str(seo.canonical_url_template),
273
+ sitemap: links.sitemap_enabled !== false, breadcrumbs: links.breadcrumbs_enabled !== false, related: links.related_pages_enabled !== false,
274
+ };
275
+ }
276
+
277
+ /** The `settings` body of a workspace PATCH: only the groups with a changed key, a cleared text sent as null. */
278
+ export function settingsPatch(was: SettingsDraft, now: SettingsDraft): Rec {
279
+ const out: Rec = {};
280
+ const put = (group: string, key: string, value: unknown): void => { out[group] = { ...(out[group] as Rec | undefined), [key]: value }; };
281
+ const text = (v: string): string | null => (v.trim() === "" ? null : v.trim());
282
+ if (now.mode !== was.mode) put("regeneration", "mode", now.mode);
283
+ if (now.approval !== was.approval) put("regeneration", "approval_mode", now.approval);
284
+ if (now.changedOnly !== was.changedOnly) put("regeneration", "changed_rows_only", now.changedOnly);
285
+ if (now.basePath !== was.basePath) put("deployment", "base_path", text(now.basePath));
286
+ if (now.domain !== was.domain) put("deployment", "domain", text(now.domain));
287
+ if (now.thinWords !== was.thinWords) put("seo", "thin_content_words", now.thinWords.trim() === "" ? null : Number(now.thinWords));
288
+ if (now.schemaRequired !== was.schemaRequired) put("seo", "schema_required", now.schemaRequired);
289
+ if (now.canonical !== was.canonical) put("seo", "canonical_url_template", text(now.canonical));
290
+ if (now.sitemap !== was.sitemap) put("links", "sitemap_enabled", now.sitemap);
291
+ if (now.breadcrumbs !== was.breadcrumbs) put("links", "breadcrumbs_enabled", now.breadcrumbs);
292
+ if (now.related !== was.related) put("links", "related_pages_enabled", now.related);
293
+ return out;
294
+ }
295
+
296
+ /** Slug preview for the form: the `{{names}}` a slug template uses. */
297
+ export function slugVariables(template: string): string[] {
298
+ return Array.from(new Set(Array.from(template.matchAll(/\{\{\s*([^{}]+?)\s*\}\}/g)).map((m) => m[1])));
299
+ }
300
+
301
+ export const PAGE_SIZE = 50;
302
+
303
+ /** The workspace (`GET /workspace`): the one read that carries the source, readiness, numbers, links and deployment. */
304
+ export interface Workspace {
305
+ campaign?: Campaign;
306
+ overview?: {
307
+ total_rows?: number | null;
308
+ generated_pages?: number | null;
309
+ live_pages?: number | null;
310
+ failed_pages?: number | null;
311
+ changed_rows?: number | null;
312
+ manual_overrides?: number | null;
313
+ last_generation?: string | null;
314
+ last_publish?: string | null;
315
+ next_regeneration?: string | null;
316
+ readiness?: { key: string; label: string; complete: boolean; details?: string[] }[];
317
+ };
318
+ template?: { template_page_id?: string | null; slug_template?: string | null; variables?: string[]; preview_row?: Rec | null };
319
+ data?: { source?: { type?: string | null; id?: string | number | null; name?: string | null }; columns?: string[]; sample_rows?: Rec[]; total_rows?: number | null; changed_rows?: number | null };
320
+ seo?: { average_score?: number | null; issues?: Record<string, number> };
321
+ links?: { orphan_pages?: string[]; sitemap_xml?: string };
322
+ deployment?: {
323
+ published_pages?: number | null;
324
+ pending_pages?: number | null;
325
+ url_pattern_preview?: string | null;
326
+ history?: { status?: string; message?: string; checked_at?: string; deployment_url?: string | null }[];
327
+ diagnostics?: { status?: string; message?: string; checked_at?: string };
328
+ };
329
+ pages?: { total?: number | null; status_counts?: Record<string, number> };
330
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @title Create a programmatic campaign and preview one page
3
+ * @when Setting up "a page per row": a template landing page (with {{variables}}) plus a list. Creating renders nothing and
4
+ * publishes nothing; the preview renders one row without saving it.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+
10
+ const campaign = await g8.api.programmaticPages.createProgrammaticCampaign({
11
+ body: {
12
+ name: "Integration pages",
13
+ template_page_id: process.env.TEMPLATE_PAGE_ID!, // a landing page with HTML, from listLandingPages
14
+ data_source_type: "list",
15
+ list_id: Number(process.env.SOURCE_LIST_ID), // from listLists
16
+ slug_template: "{{company}}-{{first_name}}",
17
+ },
18
+ });
19
+
20
+ // Programmatic responses are open objects, so name the fields you read.
21
+ const created = campaign.data as { id: string };
22
+ const preview = (await g8.api.programmaticPages.previewProgrammaticPage({ path: { campaign_id: created.id }, body: {} })).data as { slug?: string; html?: string };
23
+ console.log(created.id, preview.slug, String(preview.html ?? "").length, "characters of HTML");
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @title Render every page of a campaign and follow the run
3
+ * @when After the campaign exists. Rendering is free (no model runs). If the campaign's approval mode is "auto_publish" the API
4
+ * marks each page live as it renders, so check the mode first and have a person confirm the row count.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+ const campaign_id = process.env.CAMPAIGN_ID!;
10
+
11
+ const ws = await g8.api.programmaticPages.getProgrammaticWorkspace({ path: { campaign_id } });
12
+ const mode = (ws.data as { campaign?: { regeneration?: { approval_mode?: string } } }).campaign?.regeneration?.approval_mode;
13
+ if (mode === "auto_publish") throw new Error("This campaign publishes automatically: a person must confirm before rendering.");
14
+
15
+ await g8.api.programmaticPages.startProgrammaticGeneration({ path: { campaign_id } });
16
+ for (let i = 0; i < 120; i++) {
17
+ const status = (await g8.api.programmaticPages.getProgrammaticGenerationStatus({ path: { campaign_id } })).data as { status?: string; pages_generated?: number; pages_failed?: number; total_pages?: number };
18
+ console.log(status.status, status.pages_generated, "of", status.total_pages, "failed", status.pages_failed);
19
+ if (["completed", "failed", "paused"].includes(String(status.status))) break;
20
+ await new Promise((r) => setTimeout(r, 3000));
21
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @title List the rendered pages and publish the waiting ones after a person agrees
3
+ * @when Putting rendered pages on the live site. Publishing changes what real visitors see, so this script only reads and
4
+ * prints the numbers; the deploy call is behind PUBLISH_CONFIRMED=yes, set by a person who saw them.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+ const campaign_id = process.env.CAMPAIGN_ID!;
10
+
11
+ const ws = (await g8.api.programmaticPages.getProgrammaticWorkspace({ path: { campaign_id } })).data as { deployment?: { pending_pages?: number; published_pages?: number } };
12
+ console.log("waiting to go live:", ws.deployment?.pending_pages ?? "unknown", "live now:", ws.deployment?.published_pages ?? "unknown");
13
+
14
+ if (process.env.PUBLISH_CONFIRMED === "yes") {
15
+ const result = await g8.api.programmaticPages.runProgrammaticBulkPageAction({ path: { campaign_id }, body: { action: "deploy_pending_changes" } });
16
+ console.log(result.data);
17
+ }