@graph8/sdk 0.308.1 → 0.357.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 (654) hide show
  1. package/README.md +81 -0
  2. package/blocks/README.md +32 -0
  3. package/blocks/_foundation/README.md +78 -0
  4. package/blocks/_foundation/block.json +79 -0
  5. package/blocks/_foundation/components/api-error.tsx +62 -0
  6. package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
  7. package/blocks/_foundation/components/empty-state.tsx +24 -0
  8. package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
  9. package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
  10. package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
  11. package/blocks/_foundation/hooks/use-paginated.ts +105 -0
  12. package/blocks/_foundation/lib/g8-client.ts +128 -0
  13. package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
  14. package/blocks/_foundation/server/proxy.ts +233 -0
  15. package/blocks/_foundation/server/route.ts +30 -0
  16. package/blocks/agents/README.md +133 -0
  17. package/blocks/agents/block.json +223 -0
  18. package/blocks/agents/components/agent-builder.tsx +199 -0
  19. package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
  20. package/blocks/agents/components/agent-runs.tsx +175 -0
  21. package/blocks/agents/components/agent-skills-panel.tsx +126 -0
  22. package/blocks/agents/components/agent-test-console.tsx +175 -0
  23. package/blocks/agents/lib/agent-types.ts +114 -0
  24. package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
  25. package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
  26. package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
  27. package/blocks/analytics/README.md +73 -0
  28. package/blocks/analytics/block.json +178 -0
  29. package/blocks/analytics/components/funnel-chart.tsx +79 -0
  30. package/blocks/analytics/components/kpi-tiles.tsx +74 -0
  31. package/blocks/analytics/components/leaderboard.tsx +86 -0
  32. package/blocks/analytics/components/report-builder.tsx +200 -0
  33. package/blocks/analytics/hooks/use-report.ts +26 -0
  34. package/blocks/analytics/lib/analytics-types.ts +108 -0
  35. package/blocks/analytics/lib/use-debounced.ts +16 -0
  36. package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
  37. package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
  38. package/blocks/analytics/recipes/03-saved-report.ts +30 -0
  39. package/blocks/appointments/README.md +80 -0
  40. package/blocks/appointments/block.json +218 -0
  41. package/blocks/appointments/components/availability-editor.tsx +192 -0
  42. package/blocks/appointments/components/booking-page.tsx +154 -0
  43. package/blocks/appointments/components/event-type-editor.tsx +214 -0
  44. package/blocks/appointments/components/meeting-detail.tsx +147 -0
  45. package/blocks/appointments/components/meeting-list.tsx +148 -0
  46. package/blocks/appointments/hooks/use-available-slots.ts +29 -0
  47. package/blocks/appointments/hooks/use-schedules.ts +18 -0
  48. package/blocks/appointments/lib/appointments-types.ts +159 -0
  49. package/blocks/appointments/lib/idempotency.ts +30 -0
  50. package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
  51. package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
  52. package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
  53. package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
  54. package/blocks/block.schema.json +130 -0
  55. package/blocks/block.types.ts +109 -0
  56. package/blocks/campaigns/README.md +118 -0
  57. package/blocks/campaigns/block.json +361 -0
  58. package/blocks/campaigns/components/campaign-assets.tsx +180 -0
  59. package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
  60. package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
  61. package/blocks/campaigns/components/campaign-list.tsx +120 -0
  62. package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
  63. package/blocks/campaigns/components/campaign-select.tsx +52 -0
  64. package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
  65. package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
  66. package/blocks/campaigns/components/form-field.tsx +29 -0
  67. package/blocks/campaigns/components/icp-editor.tsx +230 -0
  68. package/blocks/campaigns/components/persona-editor.tsx +210 -0
  69. package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
  70. package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
  71. package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
  72. package/blocks/campaigns/recipes/campaign-report.ts +22 -0
  73. package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
  74. package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
  75. package/blocks/coach/README.md +93 -0
  76. package/blocks/coach/block.json +125 -0
  77. package/blocks/coach/components/call-scorecard.tsx +126 -0
  78. package/blocks/coach/components/coaching-feed.tsx +140 -0
  79. package/blocks/coach/lib/coach-types.ts +116 -0
  80. package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
  81. package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
  82. package/blocks/copilot/README.md +99 -0
  83. package/blocks/copilot/block.json +151 -0
  84. package/blocks/copilot/components/copilot-memory.tsx +87 -0
  85. package/blocks/copilot/components/copilot-panel.tsx +189 -0
  86. package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
  87. package/blocks/copilot/lib/copilot-types.ts +111 -0
  88. package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
  89. package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
  90. package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
  91. package/blocks/crm/README.md +122 -0
  92. package/blocks/crm/block.json +378 -0
  93. package/blocks/crm/components/activity-timeline.tsx +99 -0
  94. package/blocks/crm/components/company-detail.tsx +118 -0
  95. package/blocks/crm/components/company-table.tsx +173 -0
  96. package/blocks/crm/components/contact-detail.tsx +135 -0
  97. package/blocks/crm/components/contact-table.tsx +217 -0
  98. package/blocks/crm/components/dedupe-review.tsx +164 -0
  99. package/blocks/crm/components/import-wizard.tsx +193 -0
  100. package/blocks/crm/components/notes-panel.tsx +112 -0
  101. package/blocks/crm/components/record-form.tsx +191 -0
  102. package/blocks/crm/components/task-list.tsx +107 -0
  103. package/blocks/crm/hooks/use-custom-fields.ts +18 -0
  104. package/blocks/crm/hooks/use-debounced.ts +13 -0
  105. package/blocks/crm/hooks/use-page-query.ts +52 -0
  106. package/blocks/crm/lib/crm-types.ts +136 -0
  107. package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
  108. package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
  109. package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
  110. package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
  111. package/blocks/deals/README.md +78 -0
  112. package/blocks/deals/block.json +198 -0
  113. package/blocks/deals/components/deal-detail.tsx +134 -0
  114. package/blocks/deals/components/forecast-view.tsx +112 -0
  115. package/blocks/deals/components/pipeline-board.tsx +142 -0
  116. package/blocks/deals/components/stage-editor.tsx +96 -0
  117. package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
  118. package/blocks/deals/hooks/use-pipelines.ts +19 -0
  119. package/blocks/deals/lib/deals-types.ts +128 -0
  120. package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
  121. package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
  122. package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
  123. package/blocks/dialer/README.md +114 -0
  124. package/blocks/dialer/block.json +260 -0
  125. package/blocks/dialer/components/call-log.tsx +78 -0
  126. package/blocks/dialer/components/call-queue.tsx +188 -0
  127. package/blocks/dialer/components/dialer-workspace.tsx +47 -0
  128. package/blocks/dialer/components/disposition-panel.tsx +75 -0
  129. package/blocks/dialer/components/live-calls-table.tsx +107 -0
  130. package/blocks/dialer/components/session-start-form.tsx +111 -0
  131. package/blocks/dialer/components/softphone.tsx +122 -0
  132. package/blocks/dialer/components/transcript-viewer.tsx +82 -0
  133. package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
  134. package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
  135. package/blocks/dialer/hooks/use-dispositions.ts +29 -0
  136. package/blocks/dialer/lib/dialer-types.ts +158 -0
  137. package/blocks/dialer/lib/softphone-transport.ts +67 -0
  138. package/blocks/dialer/lib/twilio-transport.ts +74 -0
  139. package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
  140. package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
  141. package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
  142. package/blocks/enrichment/README.md +105 -0
  143. package/blocks/enrichment/block.json +204 -0
  144. package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
  145. package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
  146. package/blocks/enrichment/components/enrich-button.tsx +136 -0
  147. package/blocks/enrichment/components/waterfall-config.tsx +127 -0
  148. package/blocks/enrichment/hooks/use-providers.ts +18 -0
  149. package/blocks/enrichment/lib/enrichment-types.ts +104 -0
  150. package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
  151. package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
  152. package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
  153. package/blocks/forms/README.md +112 -0
  154. package/blocks/forms/block.json +200 -0
  155. package/blocks/forms/components/form-builder.tsx +150 -0
  156. package/blocks/forms/components/form-embed.tsx +103 -0
  157. package/blocks/forms/components/form-mapping-editor.tsx +125 -0
  158. package/blocks/forms/components/site-audit-panel.tsx +73 -0
  159. package/blocks/forms/components/submissions-table.tsx +158 -0
  160. package/blocks/forms/lib/form-types.ts +127 -0
  161. package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
  162. package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
  163. package/blocks/forms/recipes/route-form-to-list.ts +44 -0
  164. package/blocks/inbox/README.md +72 -0
  165. package/blocks/inbox/block.json +194 -0
  166. package/blocks/inbox/components/inbox-list.tsx +156 -0
  167. package/blocks/inbox/components/inbox-screen.tsx +49 -0
  168. package/blocks/inbox/components/reply-composer.tsx +183 -0
  169. package/blocks/inbox/components/tag-and-assign.tsx +189 -0
  170. package/blocks/inbox/components/thread-badge.tsx +17 -0
  171. package/blocks/inbox/components/thread-view.tsx +108 -0
  172. package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
  173. package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
  174. package/blocks/inbox/lib/idempotency.ts +30 -0
  175. package/blocks/inbox/lib/inbox-types.ts +100 -0
  176. package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
  177. package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
  178. package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
  179. package/blocks/integrations/README.md +75 -0
  180. package/blocks/integrations/block.json +172 -0
  181. package/blocks/integrations/components/integration-list.tsx +165 -0
  182. package/blocks/integrations/components/sync-status.tsx +223 -0
  183. package/blocks/integrations/components/webhook-manager.tsx +300 -0
  184. package/blocks/integrations/lib/integrations-utils.ts +62 -0
  185. package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
  186. package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
  187. package/blocks/integrations/recipes/register-webhook.ts +18 -0
  188. package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
  189. package/blocks/intent/README.md +57 -0
  190. package/blocks/intent/block.json +138 -0
  191. package/blocks/intent/components/account-intent-score.tsx +91 -0
  192. package/blocks/intent/components/intent-feed.tsx +215 -0
  193. package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
  194. package/blocks/intent/hooks/use-account-intent.ts +46 -0
  195. package/blocks/intent/lib/intent-utils.ts +95 -0
  196. package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
  197. package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
  198. package/blocks/intent/recipes/who-visited-url.ts +18 -0
  199. package/blocks/lifecycle/README.md +79 -0
  200. package/blocks/lifecycle/block.json +198 -0
  201. package/blocks/lifecycle/components/churn-list.tsx +92 -0
  202. package/blocks/lifecycle/components/customer-table.tsx +119 -0
  203. package/blocks/lifecycle/components/lead-queue.tsx +127 -0
  204. package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
  205. package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
  206. package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
  207. package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
  208. package/blocks/lifecycle/lib/lifecycle.ts +99 -0
  209. package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
  210. package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
  211. package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
  212. package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
  213. package/blocks/lists/README.md +96 -0
  214. package/blocks/lists/block.json +166 -0
  215. package/blocks/lists/components/add-to-list-button.tsx +111 -0
  216. package/blocks/lists/components/list-picker.tsx +94 -0
  217. package/blocks/lists/components/list-table.tsx +160 -0
  218. package/blocks/lists/components/suppression-manager.tsx +153 -0
  219. package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
  220. package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
  221. package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
  222. package/blocks/mailboxes/README.md +71 -0
  223. package/blocks/mailboxes/block.json +187 -0
  224. package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
  225. package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
  226. package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
  227. package/blocks/mailboxes/components/warmup-health.tsx +232 -0
  228. package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
  229. package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
  230. package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
  231. package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
  232. package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
  233. package/blocks/newsletter/README.md +122 -0
  234. package/blocks/newsletter/block.json +224 -0
  235. package/blocks/newsletter/components/issue-form.tsx +124 -0
  236. package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
  237. package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
  238. package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
  239. package/blocks/newsletter/components/subscriber-table.tsx +173 -0
  240. package/blocks/newsletter/lib/newsletter-types.ts +97 -0
  241. package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
  242. package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
  243. package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
  244. package/blocks/nurture/README.md +110 -0
  245. package/blocks/nurture/block.json +191 -0
  246. package/blocks/nurture/components/nurture-builder.tsx +114 -0
  247. package/blocks/nurture/components/nurture-enroll.tsx +107 -0
  248. package/blocks/nurture/components/nurture-form.tsx +120 -0
  249. package/blocks/nurture/components/nurture-stats.tsx +81 -0
  250. package/blocks/nurture/lib/nurture-types.ts +109 -0
  251. package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
  252. package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
  253. package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
  254. package/blocks/objects/README.md +120 -0
  255. package/blocks/objects/block.json +243 -0
  256. package/blocks/objects/components/attribute-field-input.tsx +82 -0
  257. package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
  258. package/blocks/objects/components/object-not-found.tsx +34 -0
  259. package/blocks/objects/components/object-record-detail.tsx +143 -0
  260. package/blocks/objects/components/object-schema-editor.tsx +197 -0
  261. package/blocks/objects/components/object-table.tsx +130 -0
  262. package/blocks/objects/components/record-form-dialog.tsx +112 -0
  263. package/blocks/objects/components/record-roles-panel.tsx +106 -0
  264. package/blocks/objects/hooks/use-object-schema.ts +36 -0
  265. package/blocks/objects/lib/object-types.ts +214 -0
  266. package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
  267. package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
  268. package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
  269. package/blocks/pages/README.md +104 -0
  270. package/blocks/pages/block.json +223 -0
  271. package/blocks/pages/components/brand-kit-editor.tsx +152 -0
  272. package/blocks/pages/components/content-library.tsx +163 -0
  273. package/blocks/pages/components/page-editor.tsx +253 -0
  274. package/blocks/pages/components/page-list.tsx +159 -0
  275. package/blocks/pages/components/seo-report.tsx +159 -0
  276. package/blocks/pages/lib/studio.ts +77 -0
  277. package/blocks/pages/recipes/audit-site-seo.ts +28 -0
  278. package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
  279. package/blocks/pages/recipes/edit-page-section.ts +25 -0
  280. package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
  281. package/blocks/quotes/README.md +79 -0
  282. package/blocks/quotes/block.json +183 -0
  283. package/blocks/quotes/components/product-picker.tsx +80 -0
  284. package/blocks/quotes/components/quote-builder.tsx +195 -0
  285. package/blocks/quotes/components/quote-list.tsx +79 -0
  286. package/blocks/quotes/components/quote-viewer.tsx +135 -0
  287. package/blocks/quotes/components/signature-status.tsx +77 -0
  288. package/blocks/quotes/hooks/use-debounced.ts +13 -0
  289. package/blocks/quotes/lib/quotes.ts +142 -0
  290. package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
  291. package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
  292. package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
  293. package/blocks/radar/README.md +106 -0
  294. package/blocks/radar/block.json +189 -0
  295. package/blocks/radar/components/competitor-battle-card.tsx +53 -0
  296. package/blocks/radar/components/competitor-tracker.tsx +158 -0
  297. package/blocks/radar/components/radar-overview.tsx +67 -0
  298. package/blocks/radar/components/radar-report.tsx +86 -0
  299. package/blocks/radar/hooks/use-competitors.ts +23 -0
  300. package/blocks/radar/lib/radar-types.ts +124 -0
  301. package/blocks/radar/recipes/battle-card.ts +28 -0
  302. package/blocks/radar/recipes/deep-dive-report.ts +32 -0
  303. package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
  304. package/blocks/radar/recipes/track-and-scan.ts +39 -0
  305. package/blocks/search/README.md +129 -0
  306. package/blocks/search/block.json +185 -0
  307. package/blocks/search/components/filter-builder.tsx +149 -0
  308. package/blocks/search/components/prospect-results.tsx +175 -0
  309. package/blocks/search/components/save-to-list-dialog.tsx +135 -0
  310. package/blocks/search/components/saved-searches.tsx +95 -0
  311. package/blocks/search/hooks/use-filter-options.ts +41 -0
  312. package/blocks/search/lib/search-types.ts +149 -0
  313. package/blocks/search/recipes/explore-filter-values.ts +27 -0
  314. package/blocks/search/recipes/saved-searches.ts +34 -0
  315. package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
  316. package/blocks/sequencer/README.md +87 -0
  317. package/blocks/sequencer/block.json +214 -0
  318. package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
  319. package/blocks/sequencer/components/sequence-builder.tsx +306 -0
  320. package/blocks/sequencer/components/sequence-list.tsx +200 -0
  321. package/blocks/sequencer/components/sequence-stats.tsx +256 -0
  322. package/blocks/sequencer/components/step-editor.tsx +213 -0
  323. package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
  324. package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
  325. package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
  326. package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
  327. package/blocks/sequencer/recipes/sequence-report.ts +22 -0
  328. package/blocks/social-ads/README.md +128 -0
  329. package/blocks/social-ads/block.json +256 -0
  330. package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
  331. package/blocks/social-ads/components/audience-sync.tsx +150 -0
  332. package/blocks/social-ads/components/creative-gallery.tsx +121 -0
  333. package/blocks/social-ads/components/post-composer.tsx +122 -0
  334. package/blocks/social-ads/components/social-post-list.tsx +116 -0
  335. package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
  336. package/blocks/social-ads/lib/social-ads-types.ts +167 -0
  337. package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
  338. package/blocks/social-ads/recipes/schedule-post.ts +51 -0
  339. package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
  340. package/blocks/visitors/README.md +56 -0
  341. package/blocks/visitors/block.json +139 -0
  342. package/blocks/visitors/components/snippet-install.tsx +169 -0
  343. package/blocks/visitors/components/traffic-chart.tsx +156 -0
  344. package/blocks/visitors/components/visitor-company-card.tsx +92 -0
  345. package/blocks/visitors/components/visitor-feed.tsx +157 -0
  346. package/blocks/visitors/lib/visitors-utils.ts +78 -0
  347. package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
  348. package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
  349. package/blocks/visitors/recipes/traffic-summary.ts +24 -0
  350. package/blocks/webchat/README.md +87 -0
  351. package/blocks/webchat/block.json +134 -0
  352. package/blocks/webchat/components/chat-widget-config.tsx +160 -0
  353. package/blocks/webchat/components/conversation-list.tsx +74 -0
  354. package/blocks/webchat/lib/webchat-types.ts +83 -0
  355. package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
  356. package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
  357. package/blocks/workbench/README.md +135 -0
  358. package/blocks/workbench/block.json +259 -0
  359. package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
  360. package/blocks/workbench/components/run-progress.tsx +132 -0
  361. package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
  362. package/blocks/workbench/components/workbench-grid.tsx +159 -0
  363. package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
  364. package/blocks/workbench/components/workbench-table-list.tsx +105 -0
  365. package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
  366. package/blocks/workbench/lib/waterfall-presets.ts +119 -0
  367. package/blocks/workbench/lib/workbench-types.ts +191 -0
  368. package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
  369. package/blocks/workbench/recipes/import-from-search.ts +42 -0
  370. package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
  371. package/blocks/workbench/recipes/waterfall-column.ts +55 -0
  372. package/blocks/workflows/README.md +100 -0
  373. package/blocks/workflows/block.json +188 -0
  374. package/blocks/workflows/components/run-history.tsx +136 -0
  375. package/blocks/workflows/components/trigger-config.tsx +134 -0
  376. package/blocks/workflows/components/workflow-editor.tsx +142 -0
  377. package/blocks/workflows/components/workflow-list.tsx +153 -0
  378. package/blocks/workflows/hooks/use-workflow.ts +51 -0
  379. package/blocks/workflows/lib/graph.ts +187 -0
  380. package/blocks/workflows/recipes/create-workflow.ts +35 -0
  381. package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
  382. package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
  383. package/blocks/workflows/recipes/set-trigger.ts +22 -0
  384. package/dist/cli.js +5330 -29
  385. package/dist/index.browser.js +7740 -0
  386. package/dist/index.browser.js.map +1 -0
  387. package/dist/index.browser.mjs +7717 -0
  388. package/dist/index.browser.mjs.map +1 -0
  389. package/dist/index.d.mts +1440 -21
  390. package/dist/index.d.ts +1440 -21
  391. package/dist/index.js +42 -3
  392. package/dist/index.js.map +1 -1
  393. package/dist/index.mjs +42 -3
  394. package/dist/index.mjs.map +1 -1
  395. package/dist/react.browser.js +7598 -0
  396. package/dist/react.browser.js.map +1 -0
  397. package/dist/react.browser.mjs +7568 -0
  398. package/dist/react.browser.mjs.map +1 -0
  399. package/dist/react.d.mts +1340 -18
  400. package/dist/react.d.ts +1340 -18
  401. package/dist/react.js +41 -2
  402. package/dist/react.js.map +1 -1
  403. package/dist/react.mjs +41 -2
  404. package/dist/react.mjs.map +1 -1
  405. package/dist/types/agentSetup.d.ts +6 -6
  406. package/dist/types/blocks/add.d.ts +53 -0
  407. package/dist/types/blocks/create.d.ts +46 -0
  408. package/dist/types/blocks/doctor.d.ts +26 -0
  409. package/dist/types/blocks/registry.d.ts +134 -0
  410. package/dist/types/cli.d.ts +16 -0
  411. package/dist/types/core.d.ts +1 -1
  412. package/dist/types/crypto-browser.d.ts +2 -0
  413. package/dist/types/crypto-node.d.ts +6 -0
  414. package/dist/types/react.d.ts +1 -1
  415. package/dist/types/version.d.ts +2 -2
  416. package/dist/types/webhooks.d.ts +2 -2
  417. package/dist/ui/index.d.mts +54 -0
  418. package/dist/ui/index.d.ts +54 -0
  419. package/dist/ui/index.js +105 -0
  420. package/dist/ui/index.js.map +1 -0
  421. package/dist/ui/index.mjs +44 -0
  422. package/dist/ui/index.mjs.map +1 -0
  423. package/dist/ui/lib/compose-refs.d.mts +19 -0
  424. package/dist/ui/lib/compose-refs.d.ts +19 -0
  425. package/dist/ui/lib/compose-refs.js +76 -0
  426. package/dist/ui/lib/compose-refs.js.map +1 -0
  427. package/dist/ui/lib/compose-refs.mjs +41 -0
  428. package/dist/ui/lib/compose-refs.mjs.map +1 -0
  429. package/dist/ui/lib/use-mobile.d.mts +4 -0
  430. package/dist/ui/lib/use-mobile.d.ts +4 -0
  431. package/dist/ui/lib/use-mobile.js +57 -0
  432. package/dist/ui/lib/use-mobile.js.map +1 -0
  433. package/dist/ui/lib/use-mobile.mjs +22 -0
  434. package/dist/ui/lib/use-mobile.mjs.map +1 -0
  435. package/dist/ui/lib/utils.d.mts +10 -0
  436. package/dist/ui/lib/utils.d.ts +10 -0
  437. package/dist/ui/lib/utils.js +33 -0
  438. package/dist/ui/lib/utils.js.map +1 -0
  439. package/dist/ui/lib/utils.mjs +9 -0
  440. package/dist/ui/lib/utils.mjs.map +1 -0
  441. package/dist/ui/primitives/alert-dialog.d.mts +22 -0
  442. package/dist/ui/primitives/alert-dialog.d.ts +22 -0
  443. package/dist/ui/primitives/alert-dialog.js +163 -0
  444. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  445. package/dist/ui/primitives/alert-dialog.mjs +119 -0
  446. package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
  447. package/dist/ui/primitives/avatar.d.mts +8 -0
  448. package/dist/ui/primitives/avatar.d.ts +8 -0
  449. package/dist/ui/primitives/avatar.js +91 -0
  450. package/dist/ui/primitives/avatar.js.map +1 -0
  451. package/dist/ui/primitives/avatar.mjs +55 -0
  452. package/dist/ui/primitives/avatar.mjs.map +1 -0
  453. package/dist/ui/primitives/breadcrumb.d.mts +13 -0
  454. package/dist/ui/primitives/breadcrumb.d.ts +13 -0
  455. package/dist/ui/primitives/breadcrumb.js +127 -0
  456. package/dist/ui/primitives/breadcrumb.js.map +1 -0
  457. package/dist/ui/primitives/breadcrumb.mjs +97 -0
  458. package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
  459. package/dist/ui/primitives/button.d.mts +15 -0
  460. package/dist/ui/primitives/button.d.ts +15 -0
  461. package/dist/ui/primitives/button.js +91 -0
  462. package/dist/ui/primitives/button.js.map +1 -0
  463. package/dist/ui/primitives/button.mjs +56 -0
  464. package/dist/ui/primitives/button.mjs.map +1 -0
  465. package/dist/ui/primitives/card.d.mts +10 -0
  466. package/dist/ui/primitives/card.d.ts +10 -0
  467. package/dist/ui/primitives/card.js +101 -0
  468. package/dist/ui/primitives/card.js.map +1 -0
  469. package/dist/ui/primitives/card.mjs +62 -0
  470. package/dist/ui/primitives/card.mjs.map +1 -0
  471. package/dist/ui/primitives/carousel.d.mts +45 -0
  472. package/dist/ui/primitives/carousel.d.ts +45 -0
  473. package/dist/ui/primitives/carousel.js +300 -0
  474. package/dist/ui/primitives/carousel.js.map +1 -0
  475. package/dist/ui/primitives/carousel.mjs +261 -0
  476. package/dist/ui/primitives/carousel.mjs.map +1 -0
  477. package/dist/ui/primitives/chart.d.mts +68 -0
  478. package/dist/ui/primitives/chart.d.ts +68 -0
  479. package/dist/ui/primitives/chart.js +280 -0
  480. package/dist/ui/primitives/chart.js.map +1 -0
  481. package/dist/ui/primitives/chart.mjs +241 -0
  482. package/dist/ui/primitives/chart.mjs.map +1 -0
  483. package/dist/ui/primitives/collapsible.d.mts +8 -0
  484. package/dist/ui/primitives/collapsible.d.ts +8 -0
  485. package/dist/ui/primitives/collapsible.js +72 -0
  486. package/dist/ui/primitives/collapsible.js.map +1 -0
  487. package/dist/ui/primitives/collapsible.mjs +36 -0
  488. package/dist/ui/primitives/collapsible.mjs.map +1 -0
  489. package/dist/ui/primitives/context-menu.d.mts +29 -0
  490. package/dist/ui/primitives/context-menu.d.ts +29 -0
  491. package/dist/ui/primitives/context-menu.js +203 -0
  492. package/dist/ui/primitives/context-menu.js.map +1 -0
  493. package/dist/ui/primitives/context-menu.mjs +155 -0
  494. package/dist/ui/primitives/context-menu.mjs.map +1 -0
  495. package/dist/ui/primitives/dialog.d.mts +26 -0
  496. package/dist/ui/primitives/dialog.d.ts +26 -0
  497. package/dist/ui/primitives/dialog.js +192 -0
  498. package/dist/ui/primitives/dialog.js.map +1 -0
  499. package/dist/ui/primitives/dialog.mjs +148 -0
  500. package/dist/ui/primitives/dialog.mjs.map +1 -0
  501. package/dist/ui/primitives/drawer.d.mts +26 -0
  502. package/dist/ui/primitives/drawer.d.ts +26 -0
  503. package/dist/ui/primitives/drawer.js +146 -0
  504. package/dist/ui/primitives/drawer.js.map +1 -0
  505. package/dist/ui/primitives/drawer.mjs +103 -0
  506. package/dist/ui/primitives/drawer.mjs.map +1 -0
  507. package/dist/ui/primitives/input.d.mts +5 -0
  508. package/dist/ui/primitives/input.d.ts +5 -0
  509. package/dist/ui/primitives/input.js +46 -0
  510. package/dist/ui/primitives/input.js.map +1 -0
  511. package/dist/ui/primitives/input.mjs +22 -0
  512. package/dist/ui/primitives/input.mjs.map +1 -0
  513. package/dist/ui/primitives/item.d.mts +27 -0
  514. package/dist/ui/primitives/item.d.ts +27 -0
  515. package/dist/ui/primitives/item.js +221 -0
  516. package/dist/ui/primitives/item.js.map +1 -0
  517. package/dist/ui/primitives/item.mjs +188 -0
  518. package/dist/ui/primitives/item.mjs.map +1 -0
  519. package/dist/ui/primitives/kbd.d.mts +15 -0
  520. package/dist/ui/primitives/kbd.d.ts +15 -0
  521. package/dist/ui/primitives/kbd.js +45 -0
  522. package/dist/ui/primitives/kbd.js.map +1 -0
  523. package/dist/ui/primitives/kbd.mjs +21 -0
  524. package/dist/ui/primitives/kbd.mjs.map +1 -0
  525. package/dist/ui/primitives/label.d.mts +6 -0
  526. package/dist/ui/primitives/label.d.ts +6 -0
  527. package/dist/ui/primitives/label.js +55 -0
  528. package/dist/ui/primitives/label.js.map +1 -0
  529. package/dist/ui/primitives/label.mjs +21 -0
  530. package/dist/ui/primitives/label.mjs.map +1 -0
  531. package/dist/ui/primitives/resizable.d.mts +10 -0
  532. package/dist/ui/primitives/resizable.d.ts +10 -0
  533. package/dist/ui/primitives/resizable.js +68 -0
  534. package/dist/ui/primitives/resizable.js.map +1 -0
  535. package/dist/ui/primitives/resizable.mjs +42 -0
  536. package/dist/ui/primitives/resizable.mjs.map +1 -0
  537. package/dist/ui/primitives/save-status.d.mts +31 -0
  538. package/dist/ui/primitives/save-status.d.ts +31 -0
  539. package/dist/ui/primitives/save-status.js +150 -0
  540. package/dist/ui/primitives/save-status.js.map +1 -0
  541. package/dist/ui/primitives/save-status.mjs +126 -0
  542. package/dist/ui/primitives/save-status.mjs.map +1 -0
  543. package/dist/ui/primitives/separator.d.mts +6 -0
  544. package/dist/ui/primitives/separator.d.ts +6 -0
  545. package/dist/ui/primitives/separator.js +62 -0
  546. package/dist/ui/primitives/separator.js.map +1 -0
  547. package/dist/ui/primitives/separator.mjs +28 -0
  548. package/dist/ui/primitives/separator.mjs.map +1 -0
  549. package/dist/ui/primitives/sheet.d.mts +16 -0
  550. package/dist/ui/primitives/sheet.d.ts +16 -0
  551. package/dist/ui/primitives/sheet.js +170 -0
  552. package/dist/ui/primitives/sheet.js.map +1 -0
  553. package/dist/ui/primitives/sheet.mjs +129 -0
  554. package/dist/ui/primitives/sheet.mjs.map +1 -0
  555. package/dist/ui/primitives/sidebar.d.mts +74 -0
  556. package/dist/ui/primitives/sidebar.d.ts +74 -0
  557. package/dist/ui/primitives/sidebar.js +697 -0
  558. package/dist/ui/primitives/sidebar.js.map +1 -0
  559. package/dist/ui/primitives/sidebar.mjs +651 -0
  560. package/dist/ui/primitives/sidebar.mjs.map +1 -0
  561. package/dist/ui/primitives/skeleton.d.mts +5 -0
  562. package/dist/ui/primitives/skeleton.d.ts +5 -0
  563. package/dist/ui/primitives/skeleton.js +40 -0
  564. package/dist/ui/primitives/skeleton.js.map +1 -0
  565. package/dist/ui/primitives/skeleton.mjs +16 -0
  566. package/dist/ui/primitives/skeleton.mjs.map +1 -0
  567. package/dist/ui/primitives/slider.d.mts +20 -0
  568. package/dist/ui/primitives/slider.d.ts +20 -0
  569. package/dist/ui/primitives/slider.js +112 -0
  570. package/dist/ui/primitives/slider.js.map +1 -0
  571. package/dist/ui/primitives/slider.mjs +78 -0
  572. package/dist/ui/primitives/slider.mjs.map +1 -0
  573. package/dist/ui/primitives/sortable.d.mts +50 -0
  574. package/dist/ui/primitives/sortable.d.ts +50 -0
  575. package/dist/ui/primitives/sortable.js +456 -0
  576. package/dist/ui/primitives/sortable.js.map +1 -0
  577. package/dist/ui/primitives/sortable.mjs +435 -0
  578. package/dist/ui/primitives/sortable.mjs.map +1 -0
  579. package/dist/ui/primitives/switch.d.mts +6 -0
  580. package/dist/ui/primitives/switch.d.ts +6 -0
  581. package/dist/ui/primitives/switch.js +67 -0
  582. package/dist/ui/primitives/switch.js.map +1 -0
  583. package/dist/ui/primitives/switch.mjs +33 -0
  584. package/dist/ui/primitives/switch.mjs.map +1 -0
  585. package/dist/ui/primitives/table.d.mts +12 -0
  586. package/dist/ui/primitives/table.d.ts +12 -0
  587. package/dist/ui/primitives/table.js +130 -0
  588. package/dist/ui/primitives/table.js.map +1 -0
  589. package/dist/ui/primitives/table.mjs +89 -0
  590. package/dist/ui/primitives/table.mjs.map +1 -0
  591. package/dist/ui/primitives/tabs.d.mts +9 -0
  592. package/dist/ui/primitives/tabs.d.ts +9 -0
  593. package/dist/ui/primitives/tabs.js +86 -0
  594. package/dist/ui/primitives/tabs.js.map +1 -0
  595. package/dist/ui/primitives/tabs.mjs +49 -0
  596. package/dist/ui/primitives/tabs.mjs.map +1 -0
  597. package/dist/ui/primitives/textarea.d.mts +5 -0
  598. package/dist/ui/primitives/textarea.d.ts +5 -0
  599. package/dist/ui/primitives/textarea.js +43 -0
  600. package/dist/ui/primitives/textarea.js.map +1 -0
  601. package/dist/ui/primitives/textarea.mjs +19 -0
  602. package/dist/ui/primitives/textarea.mjs.map +1 -0
  603. package/dist/ui/primitives/tooltip.d.mts +9 -0
  604. package/dist/ui/primitives/tooltip.d.ts +9 -0
  605. package/dist/ui/primitives/tooltip.js +94 -0
  606. package/dist/ui/primitives/tooltip.js.map +1 -0
  607. package/dist/ui/primitives/tooltip.mjs +57 -0
  608. package/dist/ui/primitives/tooltip.mjs.map +1 -0
  609. package/docs/agents.md +72 -29
  610. package/docs/api/README.md +6 -3
  611. package/docs/api/appObjects.d.ts +1 -1
  612. package/docs/api/appObjects.md +1 -1
  613. package/docs/api/by-path.md +29 -0
  614. package/docs/api/objects.d.ts +72 -5
  615. package/docs/api/objects.md +84 -7
  616. package/docs/api/processLists.d.ts +247 -0
  617. package/docs/api/processLists.md +409 -0
  618. package/docs/api/reports.d.ts +476 -0
  619. package/docs/api/reports.md +539 -0
  620. package/docs/api/scheduleBuy.d.ts +12 -8
  621. package/docs/api/scheduleBuy.md +13 -10
  622. package/docs/api/seo.d.ts +406 -3
  623. package/docs/api/seo.md +505 -1
  624. package/docs/api/workspaceBackup.d.ts +324 -0
  625. package/docs/api/workspaceBackup.md +427 -0
  626. package/package.json +174 -5
  627. package/stacks/6sense.json +46 -0
  628. package/stacks/README.md +20 -0
  629. package/stacks/apollo.json +128 -0
  630. package/stacks/clay.json +59 -0
  631. package/stacks/gong.json +72 -0
  632. package/stacks/hubspot.json +162 -0
  633. package/stacks/intercom.json +53 -0
  634. package/stacks/mailchimp.json +71 -0
  635. package/stacks/outreach.json +86 -0
  636. package/stacks/salesforce.json +94 -0
  637. package/stacks/stack.schema.json +32 -0
  638. package/stacks/starter.json +22 -0
  639. package/styles/styles.css +2 -0
  640. package/styles/theme.css +181 -0
  641. package/styles/tokens.css +284 -0
  642. package/templates/next-app/README.md.tmpl +35 -0
  643. package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
  644. package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
  645. package/templates/next-app/app/globals.css.tmpl +8 -0
  646. package/templates/next-app/app/layout.tsx.tmpl +19 -0
  647. package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
  648. package/templates/next-app/dot-env.local.example.tmpl +6 -0
  649. package/templates/next-app/dot-gitignore.tmpl +7 -0
  650. package/templates/next-app/lib/nav.ts.tmpl +2 -0
  651. package/templates/next-app/next.config.mjs.tmpl +4 -0
  652. package/templates/next-app/package.json.tmpl +25 -0
  653. package/templates/next-app/postcss.config.mjs.tmpl +4 -0
  654. package/templates/next-app/tsconfig.json.tmpl +21 -0
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AttributeFieldInput: one form control for one object attribute.
5
+ *
6
+ * Purpose: render the right input for an attribute's type (text, number, date, select, checkbox...)
7
+ * and report the raw draft value. Convert it with `fromInputValue` from ../lib/object-types.
8
+ * SDK operations: none (presentational). Used by RecordFormDialog and ObjectRecordDetail.
9
+ */
10
+
11
+ import { Input } from "@graph8/sdk/ui";
12
+ import { selectOptions } from "../lib/object-types";
13
+ import type { ObjectAttribute } from "../lib/object-types";
14
+
15
+ export interface AttributeFieldInputProps {
16
+ attribute: ObjectAttribute;
17
+ value: string | boolean;
18
+ onChange: (value: string | boolean) => void;
19
+ disabled?: boolean;
20
+ id?: string;
21
+ }
22
+
23
+ const SELECT_CLASS =
24
+ "border-input bg-background h-9 w-full rounded-md border px-3 text-sm shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50";
25
+
26
+ function inputType(type: string): string {
27
+ if (type === "number" || type === "currency" || type === "rating") return "number";
28
+ if (type === "date") return "date";
29
+ if (type === "timestamp") return "datetime-local";
30
+ if (type === "email_address") return "email";
31
+ return "text";
32
+ }
33
+
34
+ export function AttributeFieldInput({ attribute, value, onChange, disabled, id }: AttributeFieldInputProps) {
35
+ const fieldId = id ?? `g8-attr-${attribute.slug}`;
36
+ const options = selectOptions(attribute);
37
+ if (attribute.attribute_type === "checkbox") {
38
+ return (
39
+ <input
40
+ id={fieldId}
41
+ type="checkbox"
42
+ checked={value === true || value === "true"}
43
+ disabled={disabled}
44
+ onChange={(e) => onChange(e.target.checked)}
45
+ className="size-4"
46
+ />
47
+ );
48
+ }
49
+ if ((attribute.attribute_type === "select" || attribute.attribute_type === "status") && options.length > 0 && !attribute.is_multiselect) {
50
+ return (
51
+ <select id={fieldId} value={String(value)} disabled={disabled} onChange={(e) => onChange(e.target.value)} className={SELECT_CLASS}>
52
+ <option value="">{attribute.is_required ? "Choose..." : "None"}</option>
53
+ {options.map((option) => (
54
+ <option key={option} value={option}>
55
+ {option}
56
+ </option>
57
+ ))}
58
+ </select>
59
+ );
60
+ }
61
+ const hint = attribute.is_multiselect
62
+ ? "Separate several values with commas"
63
+ : attribute.attribute_type === "record_reference"
64
+ ? "Record id"
65
+ : attribute.attribute_type === "actor_reference"
66
+ ? "User id"
67
+ : undefined;
68
+ return (
69
+ <>
70
+ <Input
71
+ id={fieldId}
72
+ type={attribute.is_multiselect ? "text" : inputType(attribute.attribute_type)}
73
+ value={String(value)}
74
+ disabled={disabled}
75
+ required={attribute.is_required}
76
+ step={attribute.attribute_type === "number" || attribute.attribute_type === "currency" ? "any" : undefined}
77
+ onChange={(e) => onChange(e.target.value)}
78
+ />
79
+ {hint ? <p className="text-muted-foreground mt-1 text-xs">{hint}</p> : null}
80
+ </>
81
+ );
82
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AttributeFormDialog: add a field to an object, or edit one.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. createAttribute (POST /objects/{object_slug}/attributes) write (needs objects:manage + Admin)
8
+ * 2. updateAttribute (PATCH /objects/{object_slug}/attributes/{attr_slug}) write (title, flags, options; the type and slug are immutable)
9
+ * Options for select/status fields are typed one per line and sent as `config.options` (a list of strings).
10
+ */
11
+
12
+ import { useEffect, useState } from "react";
13
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Textarea } from "@graph8/sdk/ui";
14
+ import { ApiError } from "@/g8/components/api-error";
15
+ import { g8Patch, g8Post } from "@/g8/lib/g8-client";
16
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
17
+ import { ATTRIBUTE_TYPES, isValidSlug, selectOptions, slugify } from "../lib/object-types";
18
+ import type { ObjectAttribute } from "../lib/object-types";
19
+
20
+ export interface AttributeFormDialogProps {
21
+ open: boolean;
22
+ objectSlug: string;
23
+ /** Edit this attribute; omit to add a new one. */
24
+ attribute?: ObjectAttribute;
25
+ onClose: () => void;
26
+ onSaved: (attribute: ObjectAttribute) => void;
27
+ }
28
+
29
+ interface Draft {
30
+ title: string;
31
+ slug: string;
32
+ type: string;
33
+ required: boolean;
34
+ unique: boolean;
35
+ multiselect: boolean;
36
+ options: string;
37
+ currency: string;
38
+ }
39
+
40
+ function toDraft(attribute?: ObjectAttribute): Draft {
41
+ return {
42
+ title: attribute?.title ?? "",
43
+ slug: attribute?.slug ?? "",
44
+ type: attribute?.attribute_type ?? "text",
45
+ required: Boolean(attribute?.is_required),
46
+ unique: Boolean(attribute?.is_unique),
47
+ multiselect: Boolean(attribute?.is_multiselect),
48
+ options: attribute ? selectOptions(attribute).join("\n") : "",
49
+ currency: typeof attribute?.config?.currency_code === "string" ? attribute.config.currency_code : "USD",
50
+ };
51
+ }
52
+
53
+ function buildConfig(draft: Draft): Record<string, unknown> | undefined {
54
+ if (draft.type === "select" || draft.type === "status") {
55
+ return { options: draft.options.split("\n").map((o) => o.trim()).filter(Boolean) };
56
+ }
57
+ if (draft.type === "currency") return { currency_code: draft.currency.trim().toUpperCase() || "USD" };
58
+ return undefined;
59
+ }
60
+
61
+ export function AttributeFormDialog({ open, objectSlug, attribute, onClose, onSaved }: AttributeFormDialogProps) {
62
+ const [draft, setDraft] = useState<Draft>(() => toDraft(attribute));
63
+ const [slugTouched, setSlugTouched] = useState(false);
64
+ const base = `/objects/${encodeURIComponent(objectSlug)}/attributes`;
65
+ const needsOptions = draft.type === "select" || draft.type === "status";
66
+ const optionCount = draft.options.split("\n").filter((o) => o.trim()).length;
67
+ const valid = draft.title.trim() !== "" && isValidSlug(draft.slug) && (!needsOptions || optionCount > 0);
68
+
69
+ useEffect(() => {
70
+ if (open) {
71
+ setDraft(toDraft(attribute));
72
+ setSlugTouched(false);
73
+ }
74
+ }, [open, attribute]);
75
+
76
+ const save = useG8Mutation(async () => {
77
+ const config = buildConfig(draft);
78
+ if (attribute) {
79
+ const res = await g8Patch<ObjectAttribute>(`${base}/${encodeURIComponent(attribute.slug)}`, {
80
+ title: draft.title.trim(),
81
+ is_required: draft.required,
82
+ is_unique: draft.unique,
83
+ ...(config ? { config } : {}),
84
+ });
85
+ return res.data;
86
+ }
87
+ const res = await g8Post<ObjectAttribute>(base, {
88
+ slug: draft.slug,
89
+ title: draft.title.trim(),
90
+ attribute_type: draft.type,
91
+ is_required: draft.required,
92
+ is_unique: draft.unique,
93
+ is_multiselect: draft.multiselect,
94
+ ...(config ? { config } : {}),
95
+ });
96
+ return res.data;
97
+ }, { onSuccess: (saved) => onSaved(saved) });
98
+
99
+ const set = (patch: Partial<Draft>): void => setDraft((prev) => ({ ...prev, ...patch }));
100
+ return (
101
+ <Dialog open={open} onOpenChange={(next) => (next ? undefined : onClose())}>
102
+ <DialogContent>
103
+ <DialogHeader>
104
+ <DialogTitle>{attribute ? `Edit ${attribute.title}` : "Add a field"}</DialogTitle>
105
+ <DialogDescription>{attribute ? "The field type and key cannot change once created." : "Fields are the columns of this object."}</DialogDescription>
106
+ </DialogHeader>
107
+ <form
108
+ className="grid gap-3"
109
+ onSubmit={(e) => {
110
+ e.preventDefault();
111
+ if (valid) void save.mutate(undefined);
112
+ }}
113
+ >
114
+ <div>
115
+ <Label htmlFor="g8-attr-title" className="mb-1">Name</Label>
116
+ <Input id="g8-attr-title" value={draft.title} onChange={(e) => set({ title: e.target.value, ...(attribute || slugTouched ? {} : { slug: slugify(e.target.value) }) })} />
117
+ </div>
118
+ <div>
119
+ <Label htmlFor="g8-attr-slug" className="mb-1">Key</Label>
120
+ <Input id="g8-attr-slug" value={draft.slug} disabled={Boolean(attribute)} onChange={(e) => { setSlugTouched(true); set({ slug: e.target.value }); }} />
121
+ <p className="text-muted-foreground mt-1 text-xs">Lowercase letters, numbers and underscores. Used in API payloads.</p>
122
+ </div>
123
+ <div>
124
+ <Label htmlFor="g8-attr-type" className="mb-1">Type</Label>
125
+ <select id="g8-attr-type" value={draft.type} disabled={Boolean(attribute)} onChange={(e) => set({ type: e.target.value })} className="border-input bg-background h-9 w-full rounded-md border px-3 text-sm disabled:opacity-50">
126
+ {ATTRIBUTE_TYPES.map((t) => (
127
+ <option key={t.value} value={t.value}>{t.label}</option>
128
+ ))}
129
+ </select>
130
+ </div>
131
+ {needsOptions ? (
132
+ <div>
133
+ <Label htmlFor="g8-attr-options" className="mb-1">Options (one per line)</Label>
134
+ <Textarea id="g8-attr-options" rows={4} value={draft.options} onChange={(e) => set({ options: e.target.value })} />
135
+ </div>
136
+ ) : null}
137
+ {draft.type === "currency" ? (
138
+ <div>
139
+ <Label htmlFor="g8-attr-currency" className="mb-1">Currency code</Label>
140
+ <Input id="g8-attr-currency" value={draft.currency} maxLength={3} onChange={(e) => set({ currency: e.target.value })} />
141
+ </div>
142
+ ) : null}
143
+ <div className="flex flex-wrap gap-4 text-sm">
144
+ <label className="flex items-center gap-2"><input type="checkbox" checked={draft.required} onChange={(e) => set({ required: e.target.checked })} />Required</label>
145
+ <label className="flex items-center gap-2"><input type="checkbox" checked={draft.unique} onChange={(e) => set({ unique: e.target.checked })} />Unique</label>
146
+ {needsOptions && !attribute ? (
147
+ <label className="flex items-center gap-2"><input type="checkbox" checked={draft.multiselect} onChange={(e) => set({ multiselect: e.target.checked })} />Allow several</label>
148
+ ) : null}
149
+ </div>
150
+ {save.error ? <ApiError error={save.error} title="Could not save the field" /> : null}
151
+ <DialogFooter>
152
+ <Button type="button" variant="outline" onClick={onClose} disabled={save.loading}>Cancel</Button>
153
+ <Button type="submit" loading={save.loading} disabled={!valid}>{attribute ? "Save field" : "Add field"}</Button>
154
+ </DialogFooter>
155
+ </form>
156
+ </DialogContent>
157
+ </Dialog>
158
+ );
159
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ObjectNotFound: what to show when an object or record call answers 404.
5
+ *
6
+ * A 404 means two different things: the backend does not serve the custom objects API at all (the list call 404s: wrong base URL or an older deployment),
7
+ * or this one object/record does not exist (deleted, mistyped or stale id). The collection call `GET /objects`
8
+ * tells them apart: it is 404 only when the backend does not serve the API. (A missing scope or a proxy-blocked prefix is a 403 and shows as a normal API error.)
9
+ *
10
+ * SDK operations: listObjects (GET /objects), read.
11
+ */
12
+
13
+ import { ApiError } from "@/g8/components/api-error";
14
+ import { EmptyState } from "@/g8/components/empty-state";
15
+ import { g8Get } from "@/g8/lib/g8-client";
16
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
17
+ import { isObjectsApiMissing } from "../lib/object-types";
18
+
19
+ export interface ObjectNotFoundProps {
20
+ /** What was not found, for the message: "record" or "object". */
21
+ what: "record" | "object";
22
+ /** The error that triggered this view (only a 404 is meaningful here). */
23
+ error: unknown;
24
+ }
25
+
26
+ export function ObjectNotFound({ what, error }: ObjectNotFoundProps) {
27
+ const probe = useG8Query((signal) => g8Get<unknown[]>("/objects", {}, { signal }).then(() => "enabled" as const), []);
28
+ if (probe.loading && !probe.data && !probe.error) return <p className="text-muted-foreground text-sm" aria-busy="true">Checking...</p>;
29
+ if (isObjectsApiMissing(probe.error)) {
30
+ return <EmptyState title="Custom objects are unavailable" description="The graph8 API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current graph8 backend, then reload." />;
31
+ }
32
+ if (probe.error) return <ApiError error={probe.error} title="Could not check custom objects" onRetry={() => void probe.refetch()} />;
33
+ return <EmptyState title={`This ${what} no longer exists`} description={`It may have been deleted, or the link is out of date. Go back and pick another ${what}.`} />;
34
+ }
@@ -0,0 +1,143 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ObjectRecordDetail: one record of any object: its values, owners, relationships and change history.
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. getObject + listObjectAttributes (via useObjectSchema) read
8
+ * 2. getObjectRecord (GET /objects/{object_slug}/records/{record_id}) read
9
+ * 3. getObjectRecordChanges (GET .../records/{record_id}/changes?limit=20) read
10
+ * 4. getObjectRelationships (GET .../records/{record_id}/relationships?limit=25) read
11
+ * 5. updateObjectRecord (RecordFormDialog, sends expected_revision) write
12
+ * 6. archiveObjectRecord (DELETE .../records/{record_id}) destructive -> ConfirmSpendDialog
13
+ * 7. restoreObjectRecord (POST .../records/{record_id}/restore) write
14
+ * For contacts, companies and deals it also shows RecordRolesPanel (getRecordRoleSlots, assignRecordRole, clearRecordRole).
15
+ */
16
+
17
+ import { useEffect, useState } from "react";
18
+ import { Button, Card, CardContent, CardHeader, CardTitle, Skeleton } from "@graph8/sdk/ui";
19
+ import { ApiError } from "@/g8/components/api-error";
20
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
21
+ import { EmptyState } from "@/g8/components/empty-state";
22
+ import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
23
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
24
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
25
+ import { useObjectSchema } from "../hooks/use-object-schema";
26
+ import { ROLE_ENTITY_TYPES, formatValue, isObjectsApiMissing, recordTitle } from "../lib/object-types";
27
+ import { ObjectNotFound } from "./object-not-found";
28
+ import type { ObjectRecord } from "../lib/object-types";
29
+ import { RecordFormDialog } from "./record-form-dialog";
30
+ import { RecordRolesPanel } from "./record-roles-panel";
31
+
32
+ export interface ObjectRecordDetailProps {
33
+ objectSlug: string;
34
+ recordId: string;
35
+ /** Called after the record is archived, so the page can go back to the table. */
36
+ onArchived?: () => void;
37
+ onBack?: () => void;
38
+ }
39
+
40
+ interface ChangeEntry { id: string; action: string; occurred_at: string; actor_type?: string; changes?: Record<string, unknown> }
41
+ interface Relationship { id: string; relationship_label?: string; source_record_id: string; target_record_id: string; source_label?: string; target_label?: string }
42
+
43
+ export function ObjectRecordDetail({ objectSlug, recordId, onArchived, onBack }: ObjectRecordDetailProps) {
44
+ const base = `/objects/${encodeURIComponent(objectSlug)}/records/${encodeURIComponent(recordId)}`;
45
+ const schema = useObjectSchema(objectSlug);
46
+ const record = useG8Query((signal) => g8Get<ObjectRecord>(base, {}, { signal }).then((r) => r.data), [base]);
47
+ const changes = useG8Query((signal) => g8Get<{ entries: ChangeEntry[] }>(`${base}/changes`, { limit: 20 }, { signal }).then((r) => r.data?.entries ?? []), [base]);
48
+ const relationships = useG8Query((signal) => g8Get<{ relationships: Relationship[] }>(`${base}/relationships`, { limit: 25 }, { signal }).then((r) => r.data?.relationships ?? []), [base]);
49
+ const [editing, setEditing] = useState(false);
50
+ const [confirming, setConfirming] = useState(false);
51
+ // Dialogs belong to one record: opening another record must not leave them pointing at it.
52
+ useEffect(() => {
53
+ setEditing(false);
54
+ setConfirming(false);
55
+ }, [objectSlug, recordId]);
56
+ const refreshAll = (): void => { void record.refetch(); void changes.refetch(); };
57
+ const archive = useG8Mutation(() => g8Delete(base), { onSuccess: () => { setConfirming(false); refreshAll(); onArchived?.(); } });
58
+ const restore = useG8Mutation(() => g8Post(`${base}/restore`), { onSuccess: refreshAll });
59
+
60
+ if (isObjectsApiMissing(record.error)) return <ObjectNotFound what="record" error={record.error} />;
61
+ if (record.error) return <ApiError error={record.error} title="Could not load the record" onRetry={() => void record.refetch()} />;
62
+ if (!record.data || schema.loading) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-64" /><Skeleton className="h-40 w-full" /></div>;
63
+
64
+ const current = record.data;
65
+ const roleEntity = ROLE_ENTITY_TYPES[objectSlug];
66
+ const noun = schema.definition?.singular_noun ?? "record";
67
+ return (
68
+ <div className="grid gap-4">
69
+ <div className="flex items-center justify-between gap-2">
70
+ <div>
71
+ {onBack ? <Button variant="link" size="sm" onClick={onBack}>Back</Button> : null}
72
+ <h2 className="text-lg font-semibold">{recordTitle(schema.definition, schema.attributes, current)}</h2>
73
+ {current.is_archived ? <p className="text-muted-foreground text-sm">This record is archived.</p> : null}
74
+ </div>
75
+ <div className="flex gap-2">
76
+ {current.is_archived ? (
77
+ <Button variant="outline" loading={restore.loading} onClick={() => void restore.mutate(undefined)}>Restore</Button>
78
+ ) : (
79
+ <><Button variant="outline" onClick={() => setEditing(true)}>Edit</Button><Button variant="outline" onClick={() => { archive.reset(); setConfirming(true); }}>Archive</Button></>
80
+ )}
81
+ </div>
82
+ </div>
83
+ {restore.error ? <ApiError error={restore.error} title="Could not restore" /> : null}
84
+ <Card>
85
+ <CardHeader><CardTitle>Details</CardTitle></CardHeader>
86
+ <CardContent>
87
+ <dl className="grid gap-2 sm:grid-cols-2">
88
+ {schema.attributes.map((a) => (
89
+ <div key={a.slug}>
90
+ <dt className="text-muted-foreground text-xs">{a.title}</dt>
91
+ <dd className="text-sm">{formatValue(a, current.values?.[a.slug]) || <span className="text-muted-foreground">Not set</span>}</dd>
92
+ </div>
93
+ ))}
94
+ </dl>
95
+ </CardContent>
96
+ </Card>
97
+ {roleEntity ? <RecordRolesPanel entityType={roleEntity} entityId={recordId} /> : null}
98
+ <Card>
99
+ <CardHeader><CardTitle>Related records</CardTitle></CardHeader>
100
+ <CardContent>
101
+ {relationships.loading && !relationships.data ? <Skeleton className="h-10 w-full" /> : (relationships.data ?? []).length === 0 ? (
102
+ <p className="text-muted-foreground text-sm">Nothing linked to this record yet.</p>
103
+ ) : (
104
+ <ul className="grid gap-1 text-sm">
105
+ {(relationships.data ?? []).map((r) => (
106
+ <li key={r.id}>{r.relationship_label ?? "Linked"}: {r.source_record_id === recordId ? r.target_label ?? r.target_record_id : r.source_label ?? r.source_record_id}</li>
107
+ ))}
108
+ </ul>
109
+ )}
110
+ </CardContent>
111
+ </Card>
112
+ <Card>
113
+ <CardHeader><CardTitle>History</CardTitle></CardHeader>
114
+ <CardContent>
115
+ {changes.loading && !changes.data ? <Skeleton className="h-10 w-full" /> : (changes.data ?? []).length === 0 ? (
116
+ <p className="text-muted-foreground text-sm">No changes recorded.</p>
117
+ ) : (
118
+ <ul className="grid gap-1 text-sm">
119
+ {(changes.data ?? []).map((c) => (
120
+ <li key={c.id}>
121
+ <span className="font-medium">{c.action}</span> {new Date(c.occurred_at).toLocaleString()}
122
+ {c.actor_type ? ` by ${c.actor_type}` : ""}
123
+ {c.changes && Object.keys(c.changes).length ? ` (${Object.keys(c.changes).join(", ")})` : ""}
124
+ </li>
125
+ ))}
126
+ </ul>
127
+ )}
128
+ </CardContent>
129
+ </Card>
130
+ <RecordFormDialog open={editing} objectSlug={objectSlug} singularNoun={noun} attributes={schema.attributes} record={current} onClose={() => setEditing(false)} onSaved={() => { setEditing(false); refreshAll(); }} />
131
+ <ConfirmSpendDialog
132
+ open={confirming}
133
+ tier="destructive"
134
+ title={`Archive this ${noun}?`}
135
+ description={<><p>It leaves listings but stays readable by id, and its history is kept.</p>{archive.error ? <ApiError error={archive.error} title="Could not archive the record" /> : null}</>}
136
+ confirmLabel="Archive"
137
+ confirming={archive.loading}
138
+ onCancel={() => { archive.reset(); setConfirming(false); }}
139
+ onConfirm={() => void archive.mutate(undefined)}
140
+ />
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,197 @@
1
+ "use client";
2
+
3
+ /**
4
+ * ObjectSchemaEditor: define custom object types and their fields (the schema, not the records).
5
+ *
6
+ * SDK operations, in order:
7
+ * 1. listObjects (GET /objects) read
8
+ * 2. createObject (POST /objects) write (Admin; objects:manage)
9
+ * 3. getObject + listObjectAttributes (via useObjectSchema) read
10
+ * 4. updateObject (PATCH /objects/{object_slug}) write (labels, icon, display field)
11
+ * 5. createAttribute / updateAttribute (AttributeFormDialog) write
12
+ * 6. archiveAttribute (DELETE .../attributes/{attr_slug}) destructive -> ConfirmSpendDialog
13
+ * 7. archiveObject (DELETE /objects/{object_slug}) destructive -> ConfirmSpendDialog
14
+ * Archiving hides the object or field; records and history are kept. The API answers 404
15
+ * when the backend does not serve the objects API at all (wrong base URL or older deployment), which this component explains.
16
+ */
17
+
18
+ import { useState } from "react";
19
+ import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
20
+ import { ApiError } from "@/g8/components/api-error";
21
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
22
+ import { EmptyState } from "@/g8/components/empty-state";
23
+ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
24
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
25
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
26
+ import { useObjectSchema } from "../hooks/use-object-schema";
27
+ import { OBJECT_ICONS, isObjectsApiMissing, isValidSlug, slugify } from "../lib/object-types";
28
+ import type { ObjectAttribute, ObjectDefinition } from "../lib/object-types";
29
+ import { AttributeFormDialog } from "./attribute-form-dialog";
30
+
31
+ export interface ObjectSchemaEditorProps {
32
+ /** Object to open first. */
33
+ initialObjectSlug?: string;
34
+ /** Called when the person picks an object, so the page can show its records. */
35
+ onSelectObject?: (slug: string) => void;
36
+ }
37
+
38
+ type Pending = { kind: "attribute"; attribute: ObjectAttribute } | { kind: "object"; object: ObjectDefinition };
39
+
40
+ function NewObjectForm({ onCreated }: { onCreated: (object: ObjectDefinition) => void }) {
41
+ const [singular, setSingular] = useState("");
42
+ const [plural, setPlural] = useState("");
43
+ const [slug, setSlug] = useState("");
44
+ const [touched, setTouched] = useState(false);
45
+ const create = useG8Mutation(
46
+ async () => (await g8Post<ObjectDefinition>("/objects", { slug, singular_noun: singular.trim(), plural_noun: plural.trim() || `${singular.trim()}s` })).data,
47
+ { onSuccess: (created) => { setSingular(""); setPlural(""); setSlug(""); setTouched(false); onCreated(created); } },
48
+ );
49
+ const valid = singular.trim() !== "" && isValidSlug(slug);
50
+ return (
51
+ <form className="grid gap-2" onSubmit={(e) => { e.preventDefault(); if (valid) void create.mutate(undefined); }}>
52
+ <Label htmlFor="g8-new-object">New object</Label>
53
+ <Input id="g8-new-object" placeholder="Singular name, e.g. Invoice" value={singular} onChange={(e) => { setSingular(e.target.value); if (!touched) setSlug(slugify(e.target.value + "s")); }} />
54
+ <Input aria-label="Plural name" placeholder="Plural name (optional)" value={plural} onChange={(e) => setPlural(e.target.value)} />
55
+ <Input aria-label="Object key" placeholder="key" value={slug} onChange={(e) => { setTouched(true); setSlug(e.target.value); }} />
56
+ {create.error ? <ApiError error={create.error} title="Could not create the object" /> : null}
57
+ <Button type="submit" size="sm" loading={create.loading} disabled={!valid}>Create object</Button>
58
+ </form>
59
+ );
60
+ }
61
+
62
+ function ObjectDetailsForm({ object, attributes, onSaved }: { object: ObjectDefinition; attributes: readonly ObjectAttribute[]; onSaved: () => void }) {
63
+ const [singular, setSingular] = useState(object.singular_noun);
64
+ const [plural, setPlural] = useState(object.plural_noun);
65
+ const [icon, setIcon] = useState(object.icon ?? "");
66
+ const [display, setDisplay] = useState(object.display_attribute_slug ?? "");
67
+ const save = useG8Mutation(
68
+ async () => (await g8Patch<ObjectDefinition>(`/objects/${encodeURIComponent(object.slug)}`, { singular_noun: singular.trim(), plural_noun: plural.trim(), icon: icon || null, display_attribute_slug: display || null })).data,
69
+ { onSuccess: onSaved },
70
+ );
71
+ return (
72
+ <form className="grid gap-2 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); void save.mutate(undefined); }}>
73
+ <div><Label htmlFor="g8-obj-singular" className="mb-1">Singular name</Label><Input id="g8-obj-singular" value={singular} onChange={(e) => setSingular(e.target.value)} /></div>
74
+ <div><Label htmlFor="g8-obj-plural" className="mb-1">Plural name</Label><Input id="g8-obj-plural" value={plural} onChange={(e) => setPlural(e.target.value)} /></div>
75
+ <div>
76
+ <Label htmlFor="g8-obj-icon" className="mb-1">Icon</Label>
77
+ <select id="g8-obj-icon" value={icon} onChange={(e) => setIcon(e.target.value)} className="border-input bg-background h-9 w-full rounded-md border px-3 text-sm">
78
+ <option value="">Default</option>
79
+ {OBJECT_ICONS.map((i) => <option key={i} value={i}>{i}</option>)}
80
+ </select>
81
+ </div>
82
+ <div>
83
+ <Label htmlFor="g8-obj-display" className="mb-1">Record title field</Label>
84
+ <select id="g8-obj-display" value={display} onChange={(e) => setDisplay(e.target.value)} className="border-input bg-background h-9 w-full rounded-md border px-3 text-sm">
85
+ <option value="">Automatic</option>
86
+ {attributes.map((a) => <option key={a.slug} value={a.slug}>{a.title}</option>)}
87
+ </select>
88
+ </div>
89
+ {save.error ? <div className="sm:col-span-2"><ApiError error={save.error} title="Could not save the object" /></div> : null}
90
+ <div className="sm:col-span-2"><Button type="submit" size="sm" loading={save.loading}>Save object</Button></div>
91
+ </form>
92
+ );
93
+ }
94
+
95
+ export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: ObjectSchemaEditorProps) {
96
+ const [selected, setSelected] = useState<string | undefined>(initialObjectSlug);
97
+ const [editing, setEditing] = useState<ObjectAttribute | "new" | null>(null);
98
+ const [pending, setPending] = useState<Pending | null>(null);
99
+ const objects = useG8Query((signal) => g8Get<ObjectDefinition[]>("/objects", {}, { signal }).then((r) => r.data ?? []), []);
100
+ const schema = useObjectSchema(selected);
101
+ const choose = (slug: string): void => { setSelected(slug); onSelectObject?.(slug); };
102
+
103
+ const archive = useG8Mutation(async (target: Pending) => {
104
+ if (target.kind === "attribute") await g8Delete(`/objects/${encodeURIComponent(selected ?? "")}/attributes/${encodeURIComponent(target.attribute.slug)}`);
105
+ else await g8Delete(`/objects/${encodeURIComponent(target.object.slug)}`);
106
+ return target;
107
+ }, {
108
+ onSuccess: (target) => {
109
+ setPending(null);
110
+ if (target.kind === "object") { setSelected(undefined); void objects.refetch(); } else void schema.refetch();
111
+ },
112
+ });
113
+
114
+ if (objects.loading && !objects.data) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-48" /><Skeleton className="h-40 w-full" /></div>;
115
+ if (isObjectsApiMissing(objects.error)) {
116
+ return <EmptyState title="Custom objects are unavailable" description="The graph8 API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current graph8 backend, then reload." />;
117
+ }
118
+ if (objects.error) return <ApiError error={objects.error} title="Could not load objects" onRetry={() => void objects.refetch()} />;
119
+
120
+ const list = (objects.data ?? []).filter((o) => !o.is_archived);
121
+ const current = list.find((o) => o.slug === selected);
122
+ return (
123
+ <div className="grid gap-4 md:grid-cols-[16rem_1fr]">
124
+ <Card>
125
+ <CardHeader><CardTitle>Objects</CardTitle></CardHeader>
126
+ <CardContent className="grid gap-4">
127
+ {list.length === 0 ? <p className="text-muted-foreground text-sm">No objects yet. Create the first one below.</p> : (
128
+ <ul className="grid gap-1">
129
+ {list.map((o) => (
130
+ <li key={o.slug}>
131
+ <Button variant={o.slug === selected ? "secondary" : "ghost"} size="sm" className="w-full justify-start" onClick={() => choose(o.slug)}>
132
+ {o.plural_noun}{o.is_system ? <span className="text-muted-foreground ml-2 text-xs">standard</span> : null}
133
+ </Button>
134
+ </li>
135
+ ))}
136
+ </ul>
137
+ )}
138
+ <NewObjectForm onCreated={(created) => { void objects.refetch(); choose(created.slug); }} />
139
+ </CardContent>
140
+ </Card>
141
+ <Card>
142
+ {!current ? <CardContent><EmptyState title="Pick an object" description="Choose an object on the left to edit its fields, or create a new one." /></CardContent> : (
143
+ <>
144
+ <CardHeader><CardTitle>{current.plural_noun}</CardTitle></CardHeader>
145
+ <CardContent className="grid gap-4">
146
+ {schema.error ? <ApiError error={schema.error} title="Could not load the fields" onRetry={() => void schema.refetch()} /> : null}
147
+ {!current.is_system ? <ObjectDetailsForm key={current.slug} object={current} attributes={schema.attributes} onSaved={() => void objects.refetch()} /> : <p className="text-muted-foreground text-sm">Standard objects can gain fields but cannot be renamed or archived.</p>}
148
+ <div className="flex items-center justify-between">
149
+ <h3 className="text-sm font-semibold">Fields</h3>
150
+ <Button size="sm" onClick={() => setEditing("new")}>Add field</Button>
151
+ </div>
152
+ {schema.loading ? <Skeleton className="h-24 w-full" /> : schema.attributes.length === 0 ? (
153
+ <EmptyState title="No fields yet" description="Add a field to start capturing data on this object." />
154
+ ) : (
155
+ <Table>
156
+ <TableHeader><TableRow><TableHead>Name</TableHead><TableHead>Key</TableHead><TableHead>Type</TableHead><TableHead /></TableRow></TableHeader>
157
+ <TableBody>
158
+ {schema.attributes.map((a) => (
159
+ <TableRow key={a.slug}>
160
+ <TableCell>{a.title}{a.is_required ? " *" : ""}</TableCell>
161
+ <TableCell className="font-mono text-xs">{a.slug}</TableCell>
162
+ <TableCell>{a.attribute_type}</TableCell>
163
+ <TableCell className="text-right">
164
+ {a.is_system ? null : (<><Button variant="ghost" size="sm" onClick={() => setEditing(a)}>Edit</Button><Button variant="ghost" size="sm" onClick={() => { archive.reset(); setPending({ kind: "attribute", attribute: a }); }}>Archive</Button></>)}
165
+ </TableCell>
166
+ </TableRow>
167
+ ))}
168
+ </TableBody>
169
+ </Table>
170
+ )}
171
+ {!current.is_system ? <div><Button variant="outline" size="sm" onClick={() => { archive.reset(); setPending({ kind: "object", object: current }); }}>Archive {current.plural_noun}</Button></div> : null}
172
+ </CardContent>
173
+ </>
174
+ )}
175
+ </Card>
176
+ {selected ? (
177
+ <AttributeFormDialog
178
+ open={editing !== null}
179
+ objectSlug={selected}
180
+ attribute={editing && editing !== "new" ? editing : undefined}
181
+ onClose={() => setEditing(null)}
182
+ onSaved={() => { setEditing(null); void schema.refetch(); }}
183
+ />
184
+ ) : null}
185
+ <ConfirmSpendDialog
186
+ open={pending !== null}
187
+ tier="destructive"
188
+ title={pending?.kind === "object" ? `Archive ${pending.object.plural_noun}?` : `Archive the field "${pending?.attribute.title ?? ""}"?`}
189
+ description={<><p>{pending?.kind === "object" ? "The object disappears from the app. Its records and history are kept." : "The field stops appearing on records. Stored values and history are kept."}</p>{archive.error ? <ApiError error={archive.error} title="Could not archive" /> : null}</>}
190
+ confirmLabel="Archive"
191
+ confirming={archive.loading}
192
+ onCancel={() => { archive.reset(); setPending(null); }}
193
+ onConfirm={() => { if (pending) void archive.mutate(pending); }}
194
+ />
195
+ </div>
196
+ );
197
+ }