@graph8/sdk 0.373.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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -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
+ }
@@ -11,6 +11,12 @@ Install with `npx @graph8/sdk add quotes`.
11
11
  | `QuoteViewer` | One quote with totals and status-based actions: send/resend, edit as draft, duplicate, delete. |
12
12
  | `SignatureStatus` | Envelope state, signers, activity, reminder. |
13
13
  | `QuoteLifecycleActions` | PDF download, archive and unarchive, and for a sent quote expire, revoke and mark accepted. Each risky action is confirmed. |
14
+ | `QuoteTemplates` | Quote templates: create, edit (a full replace), delete; terms, validity, contract length, attached documents, preset lines. |
15
+ | `QuoteSettings` | Org-wide quote settings: branding, terms, numbering, who quotes are sent as, billing provider, deal automation, saved terms. |
16
+ | `QuoteDocuments` | The legal document library (NDA, MSA, other PDFs) that templates attach. |
17
+ | `QuoteEvidence` | Internal PDF evidence on one quote: list, attach, download. |
18
+ | `QuoteBatchActions` | Archive, expire or revoke the ticked quotes in one confirmed, id-addressed request (mounted by `QuoteList selectable`). |
19
+ | `ManualProducts` | The org's own product catalogue: list, add, edit, retire. |
14
20
  | `CreateQuoteFromDeal` | A "Create quote from this deal" button (builder in a side sheet, then the saved quote). Pass it to `DealsScreen`'s `renderQuoteActions`. |
15
21
 
16
22
  ## Screens
@@ -26,6 +32,24 @@ Install with `npx @graph8/sdk add quotes`.
26
32
  destructive confirms) and **Mark accepted**. Mark accepted raises an invoice with your billing provider, so it is an
27
33
  external confirm that also requires an evidence type (signed offline, customer email, verbal authorization, payment
28
34
  received) and a note. Use it only when the customer paid or signed outside the product.
35
+ - **Batch** (`QuoteList selectable`, on in the screen): tick quotes, then Archive, Expire or Revoke. The request carries the ticked quote
36
+ ids and nothing else (never a filter); a quote the action does not apply to (expire a draft, revoke a quote that is not out for
37
+ signature, archive an archived one) is left out of the request and out of the confirm count, and the confirm says how many were
38
+ left out. The result is read quote by quote ("N of M archived", with the reason for each skipped or failed one). Ticks clear when the
39
+ filter or page changes and prune to the quotes still listed after a reload. Max 200 per request.
40
+ - **Templates tab**: list, create, edit and delete quote templates. Saving a template REPLACES it, and the API body cannot carry
41
+ accepted payment methods, marketing rights or the contract-start-on-signature setting, so a template that has any of those asks
42
+ first and names which ones the save resets. Delete is a destructive confirm (quotes already built from the template are unchanged).
43
+ - **Settings tab**: org-wide settings and the document library. Only changed fields are sent. The API ignores a null, so a field that has
44
+ a value cannot be cleared from here: the form says so and holds Save instead of reporting a save that changed nothing. Changing who
45
+ quotes are sent as, the billing provider (Manual or Stripe only: QuickBooks, Xero and Chargebee are not built) or the deal automation
46
+ confirms first and states the effect. Document uploads take PDFs only, up to about 5 MB (the app proxy caps a whole request at 5 MiB;
47
+ the API allows 10 MB).
48
+ - **Products tab**: the quotable catalogue, plus the manual products you add, edit (changed fields only) and retire (a soft delete; quotes
49
+ that already include the product keep the line item).
50
+ - **Evidence tab** of an open quote: attach internal PDFs (checked before upload; the API has no delete, so an attached file stays) and
51
+ download them through the API's 60 second link.
52
+ - **Preview email**: inside the send confirm, renders the exact email and attachments without sending anything.
29
53
  - The Deal tab of a quote offers "Create another quote" for the same deal.
30
54
  - **New quote** asks for the deal first (search by name, skipped when `dealId` is given because graph8 attaches every
31
55
  quote to a deal), then opens the builder (details, signer, line items from the quotable products, live totals).
@@ -56,7 +80,7 @@ The deal search calls `GET /deals`, so the key also needs `deals:read` and the p
56
80
 
57
81
  ## Prerequisites
58
82
 
59
- - `foundation` installed (and `deals`, which supplies the `dealId`); proxy `allowedPrefixes` include `quotes`, `quotable-products`, `quote-contacts`, `envelopes`.
83
+ - `foundation` installed (and `deals`, which supplies the `dealId`); proxy `allowedPrefixes` include `quotes`, `quotable-products`, `quote-contacts`, `quote-templates`, `quote-settings`, `manual-products`, `envelopes` (derived from the block's operations; `doctor --fix-prefixes` adds them to an existing app). Templates, settings, documents and manual products need an organization admin key to change.
60
84
  - `where: "api"`: at least one quotable product.
61
85
  - `where: "app"`: quote settings (company legal name, terms, sender) filled in; Stripe connected for Stripe products and subscriptions.
62
86
 
@@ -10,15 +10,28 @@
10
10
  "send, resend and edit-as-draft behind confirms",
11
11
  "e-signature status with reminder",
12
12
  "quote lifecycle: PDF download, archive and unarchive, expire, revoke and mark accepted",
13
- "create a quote from a deal anywhere with one button"
13
+ "create a quote from a deal anywhere with one button",
14
+ "quote templates: create, edit and delete, with attached documents and preset lines",
15
+ "org quote settings and the legal document library",
16
+ "internal PDF evidence on a quote",
17
+ "archive, expire or revoke several quotes at once, id-addressed and confirmed",
18
+ "manual product catalogue: add, edit and retire",
19
+ "preview the exact email before sending a quote"
14
20
  ],
15
21
  "dependsOn": [
16
22
  "deals"
17
23
  ],
18
24
  "operations": [
19
25
  "archive_quote_quotes__quote_id__archive_post",
26
+ "batch_quote_action_quotes_batch_post",
27
+ "create_manual_product_manual_products_post",
20
28
  "create_quote_quotes_post",
29
+ "create_quote_template_quote_templates_post",
30
+ "delete_manual_product_manual_products__product_id__delete",
31
+ "delete_quote_document_quote_settings_documents__doc_id__delete",
21
32
  "delete_quote_quotes__quote_id__delete",
33
+ "delete_quote_template_quote_templates__template_id__delete",
34
+ "download_quote_evidence_quotes__quote_id__evidence__evidence_id__get",
22
35
  "duplicate_quote_quotes__quote_id__duplicate_post",
23
36
  "edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
24
37
  "expire_quote_quotes__quote_id__expire_post",
@@ -27,22 +40,33 @@
27
40
  "get_deal_deals__deal_id__get",
28
41
  "get_quote_pdf_quotes__quote_id__pdf_get",
29
42
  "get_quote_quotes__quote_id__get",
43
+ "get_quote_settings_quote_settings_get",
30
44
  "list_deals_deals_get",
45
+ "list_manual_products_manual_products_get",
31
46
  "list_quotable_products_quotable_products_get",
47
+ "list_quote_documents_quote_settings_documents_get",
48
+ "list_quote_evidence_quotes__quote_id__evidence_get",
49
+ "list_quote_templates_quote_templates_get",
32
50
  "list_quotes_quotes_get",
33
51
  "mark_quote_accepted_quotes__quote_id__mark_accepted_post",
52
+ "preview_quote_send_quotes__quote_id__send_preview_post",
34
53
  "resend_envelope_envelopes__envelope_id__resend_post",
35
54
  "revoke_quote_quotes__quote_id__revoke_post",
36
55
  "search_quote_contacts_quote_contacts_get",
37
56
  "send_quote_quotes__quote_id__send_post",
38
57
  "unarchive_quote_quotes__quote_id__unarchive_post",
39
- "update_quote_quotes__quote_id__patch"
58
+ "update_manual_product_manual_products__product_id__patch",
59
+ "update_quote_quotes__quote_id__patch",
60
+ "update_quote_settings_quote_settings_put",
61
+ "update_quote_template_quote_templates__template_id__put",
62
+ "upload_quote_document_quote_settings_documents_post",
63
+ "upload_quote_evidence_quotes__quote_id__evidence_post"
40
64
  ],
41
65
  "components": [
42
66
  {
43
67
  "name": "QuoteList",
44
68
  "file": "components/quote-list.tsx",
45
- "purpose": "Quotes with a status filter, totals in the quote currency, and paging; optionally scoped to a deal or company.",
69
+ "purpose": "Quotes with a status filter, totals in the quote currency, and paging; optionally scoped to a deal or company. With selectable, ticked quotes can be archived, expired or revoked together.",
46
70
  "operations": [
47
71
  "list_quotes_quotes_get"
48
72
  ],
@@ -79,13 +103,14 @@
79
103
  {
80
104
  "name": "QuoteViewer",
81
105
  "file": "components/quote-viewer.tsx",
82
- "purpose": "Read a quote with its totals and actions by status: send or resend for signature, edit as draft, duplicate, delete.",
106
+ "purpose": "Read a quote with its totals and actions by status: send or resend for signature, edit as draft, duplicate, delete. Previews the exact email from the send confirm.",
83
107
  "operations": [
84
108
  "get_quote_quotes__quote_id__get",
85
109
  "send_quote_quotes__quote_id__send_post",
86
110
  "edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
87
111
  "duplicate_quote_quotes__quote_id__duplicate_post",
88
- "delete_quote_quotes__quote_id__delete"
112
+ "delete_quote_quotes__quote_id__delete",
113
+ "preview_quote_send_quotes__quote_id__send_preview_post"
89
114
  ],
90
115
  "tiers": [
91
116
  "read",
@@ -118,6 +143,93 @@
118
143
  "read"
119
144
  ]
120
145
  },
146
+ {
147
+ "name": "QuoteTemplates",
148
+ "file": "components/quote-templates.tsx",
149
+ "purpose": "Quote templates: list, create, edit (a full replace) and delete; terms, validity, contract length, attached documents and preset lines. Delete is confirmed, and so is a save that would reset settings the API cannot carry.",
150
+ "operations": [
151
+ "list_quote_templates_quote_templates_get",
152
+ "create_quote_template_quote_templates_post",
153
+ "update_quote_template_quote_templates__template_id__put",
154
+ "delete_quote_template_quote_templates__template_id__delete",
155
+ "list_quote_documents_quote_settings_documents_get",
156
+ "list_quotable_products_quotable_products_get"
157
+ ],
158
+ "tiers": [
159
+ "read",
160
+ "external",
161
+ "destructive"
162
+ ]
163
+ },
164
+ {
165
+ "name": "QuoteSettings",
166
+ "file": "components/quote-settings.tsx",
167
+ "purpose": "Org-wide quote settings (branding, terms, numbering, sender identity, billing provider, deal automation, required billing details, saved terms). Sends only changed fields, refuses to pretend a value was cleared, and confirms changes to sender identity, billing provider and deal automation.",
168
+ "operations": [
169
+ "get_quote_settings_quote_settings_get",
170
+ "update_quote_settings_quote_settings_put"
171
+ ],
172
+ "tiers": [
173
+ "read",
174
+ "external"
175
+ ]
176
+ },
177
+ {
178
+ "name": "QuoteDocuments",
179
+ "file": "components/quote-documents.tsx",
180
+ "purpose": "The legal document library (NDA, MSA, other PDFs): list, upload with client-side PDF and size checks, delete behind a confirm.",
181
+ "operations": [
182
+ "list_quote_documents_quote_settings_documents_get",
183
+ "upload_quote_document_quote_settings_documents_post",
184
+ "delete_quote_document_quote_settings_documents__doc_id__delete"
185
+ ],
186
+ "tiers": [
187
+ "read",
188
+ "external",
189
+ "destructive"
190
+ ]
191
+ },
192
+ {
193
+ "name": "QuoteEvidence",
194
+ "file": "components/quote-evidence.tsx",
195
+ "purpose": "Internal PDF evidence on a quote: list, attach (checked before upload; cannot be removed afterwards) and download via the API's short-lived link.",
196
+ "operations": [
197
+ "list_quote_evidence_quotes__quote_id__evidence_get",
198
+ "upload_quote_evidence_quotes__quote_id__evidence_post",
199
+ "download_quote_evidence_quotes__quote_id__evidence__evidence_id__get"
200
+ ],
201
+ "tiers": [
202
+ "read",
203
+ "write"
204
+ ]
205
+ },
206
+ {
207
+ "name": "QuoteBatchActions",
208
+ "file": "components/quote-batch-actions.tsx",
209
+ "purpose": "Archive, expire or revoke the ticked quotes in one confirmed request addressed by quote id; reads the per-quote result.",
210
+ "operations": [
211
+ "batch_quote_action_quotes_batch_post"
212
+ ],
213
+ "tiers": [
214
+ "external"
215
+ ]
216
+ },
217
+ {
218
+ "name": "ManualProducts",
219
+ "file": "components/manual-products.tsx",
220
+ "purpose": "The org's own product catalogue: list (including inactive), add, edit changed fields only, and retire behind a confirm.",
221
+ "operations": [
222
+ "list_manual_products_manual_products_get",
223
+ "create_manual_product_manual_products_post",
224
+ "update_manual_product_manual_products__product_id__patch",
225
+ "delete_manual_product_manual_products__product_id__delete"
226
+ ],
227
+ "tiers": [
228
+ "read",
229
+ "write",
230
+ "destructive"
231
+ ]
232
+ },
121
233
  {
122
234
  "name": "ProductCatalog",
123
235
  "file": "components/quote-related.tsx",
@@ -180,6 +292,10 @@
180
292
  {
181
293
  "file": "lib/quotes.ts",
182
294
  "purpose": "Line-item math in cents, product and line types, request line mapping, status labels."
295
+ },
296
+ {
297
+ "file": "lib/quote-admin.ts",
298
+ "purpose": "Rules for the admin screens: template save body and what a save resets, settings diff (only changed fields, what the API cannot clear), PDF upload checks, batch eligibility and result reading."
183
299
  }
184
300
  ],
185
301
  "recipes": [
@@ -223,7 +339,8 @@
223
339
  "deals:read",
224
340
  "quotes:delete",
225
341
  "quotes:read",
226
- "quotes:run"
342
+ "quotes:run",
343
+ "quotes:write"
227
344
  ],
228
345
  "defaults": [
229
346
  {