@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.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SiteAuditPanel: forms on your site that graph8 is NOT capturing, from the latest crawl.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getSiteAudit (GET /forms/site-audit) read (findings of the last crawl)
|
|
8
|
+
* 2. requestSiteAuditScan (POST /forms/site-audit/scan) write (crawl again; asynchronous, one per org per minute)
|
|
9
|
+
* 3. updateSiteAuditFinding (PATCH /forms/site-audit/findings/{id}) write (acknowledge, ignore or resolve)
|
|
10
|
+
* The scan is queued, not finished: the panel re-reads the audit a few times after you ask (useJobPoll) until the
|
|
11
|
+
* last scan time changes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
22
|
+
|
|
23
|
+
interface Finding { id: string; severity?: string; page_url?: string; form_selector?: string; evidence?: string; recommendation?: string; status: string }
|
|
24
|
+
interface Audit { last_scan_at: string | null; findings: Finding[]; summary?: { open?: number } }
|
|
25
|
+
|
|
26
|
+
export function SiteAuditPanel() {
|
|
27
|
+
const audit = useG8Query((signal) => g8Get<Audit>("/forms/site-audit", {}, { signal }).then((r) => r.data), []);
|
|
28
|
+
const before = audit.data?.last_scan_at ?? null;
|
|
29
|
+
const poll = useJobPoll((signal) => g8Get<Audit>("/forms/site-audit", {}, { signal }).then((r) => r.data), {
|
|
30
|
+
enabled: false,
|
|
31
|
+
intervalMs: 3000,
|
|
32
|
+
maxAttempts: 20,
|
|
33
|
+
isTerminal: (a) => a.last_scan_at !== before,
|
|
34
|
+
onDone: () => void audit.refetch(),
|
|
35
|
+
});
|
|
36
|
+
const scan = useG8Mutation(() => g8Post("/forms/site-audit/scan"), { onSuccess: () => poll.start() });
|
|
37
|
+
const [busyId, setBusyId] = useState<string | null>(null);
|
|
38
|
+
const triage = useG8Mutation((v: { id: string; status: string }) => g8Patch(`/forms/site-audit/findings/${encodeURIComponent(v.id)}`, { status: v.status }), { onSuccess: () => void audit.refetch() });
|
|
39
|
+
const triageOne = async (id: string, status: string): Promise<void> => {
|
|
40
|
+
setBusyId(id);
|
|
41
|
+
await triage.mutate({ id, status });
|
|
42
|
+
setBusyId(null);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (audit.error) return <ApiError error={audit.error} title="Could not load the site audit" onRetry={() => void audit.refetch()} />;
|
|
46
|
+
if (audit.loading && !audit.data) return <Skeleton className="h-24 w-full" aria-busy="true" />;
|
|
47
|
+
const open = (audit.data?.findings ?? []).filter((f) => f.status === "open" || f.status === "acknowledged");
|
|
48
|
+
return (
|
|
49
|
+
<section className="grid gap-3" aria-label="Site audit">
|
|
50
|
+
<div className="flex items-center justify-between">
|
|
51
|
+
<p className="text-muted-foreground text-sm">{audit.data?.last_scan_at ? `Last scan ${new Date(audit.data.last_scan_at).toLocaleString()}` : "Never scanned"}</p>
|
|
52
|
+
<Button size="sm" loading={scan.loading || poll.polling} onClick={() => void scan.mutate(undefined)}>{poll.polling ? "Scanning..." : "Scan my site again"}</Button>
|
|
53
|
+
</div>
|
|
54
|
+
{scan.error ? <ApiError error={scan.error} title="Could not start a scan" /> : null}
|
|
55
|
+
{triage.error ? <ApiError error={triage.error} title="Could not update the finding" /> : null}
|
|
56
|
+
{open.length === 0 ? <EmptyState title="No uncaptured forms found" description="Every form the last crawl found is being captured." /> : (
|
|
57
|
+
<ul className="grid gap-2">
|
|
58
|
+
{open.map((f) => (
|
|
59
|
+
<li key={f.id} className="rounded-md border p-3 text-sm">
|
|
60
|
+
<p className="font-medium">{f.severity ? `${f.severity}: ` : ""}{f.evidence ?? f.form_selector}</p>
|
|
61
|
+
<p className="text-muted-foreground truncate">{f.page_url}</p>
|
|
62
|
+
{f.recommendation ? <p>{f.recommendation}</p> : null}
|
|
63
|
+
<div className="mt-2 flex gap-2">
|
|
64
|
+
<Button size="sm" variant="outline" loading={busyId === f.id} disabled={busyId !== null} onClick={() => void triageOne(f.id, "ignored")}>Ignore</Button>
|
|
65
|
+
<Button size="sm" variant="outline" disabled={busyId !== null} onClick={() => void triageOne(f.id, "resolved")}>Mark resolved</Button>
|
|
66
|
+
</div>
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ul>
|
|
70
|
+
)}
|
|
71
|
+
</section>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SubmissionsTable: what people submitted to one form, what happened to each lead, and a retry for failures.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. searchFormSubmissions (POST /forms/{form_key}/submissions) read (a read; POST only because filters are a body)
|
|
8
|
+
* 2. getFormSubmissionTrace (GET .../submissions/{submission_id}/trace) read ("the customer filled in the form, where did it go")
|
|
9
|
+
* 3. retryFormDelivery (POST /forms/deliveries/{receipt_id}/retry) external -> ConfirmSpendDialog (re-delivers ONE lead)
|
|
10
|
+
* Identifying fields are redacted for keys without the Signals write capability: an empty-looking cell can be a
|
|
11
|
+
* redaction, not a missing value. Test submissions are marked.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { cellText, pickItems, submissionFailed, submissionState } from "../lib/form-types";
|
|
23
|
+
import type { FormRef, FormSubmission } from "../lib/form-types";
|
|
24
|
+
|
|
25
|
+
export interface SubmissionsTableProps {
|
|
26
|
+
/** The form to show. Omit it (for example as a stack nav page) and a form picker is shown instead. */
|
|
27
|
+
form?: FormRef;
|
|
28
|
+
/** Rows per page. Default 20. */
|
|
29
|
+
pageSize?: number;
|
|
30
|
+
/** Max submitted-field columns. Default 4. */
|
|
31
|
+
maxFieldColumns?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Page { items: FormSubmission[]; total: number }
|
|
35
|
+
|
|
36
|
+
function SubmissionTrace({ form, submission, onClose }: { form: FormRef; submission: FormSubmission | null; onClose: () => void }) {
|
|
37
|
+
const trace = useG8Query(
|
|
38
|
+
(signal) => g8Get<{ capture_state?: string; receipts?: unknown[] }>(`/forms/${encodeURIComponent(form.form_key)}/submissions/${encodeURIComponent(submission?.id ?? "")}/trace`, { source_id: form.source_id }, { signal }).then((r) => r.data),
|
|
39
|
+
[form.form_key, form.source_id, submission?.id],
|
|
40
|
+
{ enabled: Boolean(submission) },
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<Dialog open={submission !== null} onOpenChange={(open) => (open ? undefined : onClose())}>
|
|
44
|
+
<DialogContent className="max-h-[80vh] overflow-y-auto">
|
|
45
|
+
<DialogHeader>
|
|
46
|
+
<DialogTitle>Where did this lead go?</DialogTitle>
|
|
47
|
+
<DialogDescription>Every delivery step for this submission, in order.</DialogDescription>
|
|
48
|
+
</DialogHeader>
|
|
49
|
+
{trace.loading ? <Skeleton className="h-24 w-full" /> : trace.error ? <ApiError error={trace.error} title="No trace for this submission" /> : (
|
|
50
|
+
<div className="grid gap-2 text-sm">
|
|
51
|
+
<p>Capture: <strong>{trace.data?.capture_state ?? "unknown"}</strong></p>
|
|
52
|
+
{pickItems<Record<string, unknown>>(trace.data, "receipts").map((r, i) => (
|
|
53
|
+
<pre key={i} className="overflow-x-auto rounded border p-2 text-xs">{JSON.stringify(r, null, 2)}</pre>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</DialogContent>
|
|
58
|
+
</Dialog>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function SubmissionsTable({ form: given, pageSize = 20, maxFieldColumns = 4 }: SubmissionsTableProps) {
|
|
63
|
+
const [picked, setPicked] = useState<FormRef | undefined>(undefined);
|
|
64
|
+
const forms = useG8Query((signal) => g8Get<unknown>("/forms", { per_page: 100 }, { signal }).then((r) => pickItems<FormRef & { display_name?: string | null }>(r.data, "items")), [], { enabled: !given });
|
|
65
|
+
const form = given ?? picked;
|
|
66
|
+
if (!form) {
|
|
67
|
+
if (forms.error) return <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} />;
|
|
68
|
+
if (forms.loading && !forms.data) return <Skeleton className="h-9 w-64" aria-busy="true" />;
|
|
69
|
+
if ((forms.data ?? []).length === 0) return <EmptyState title="No forms captured yet" description="Install the graph8 snippet on your site; forms appear after their first submission." />;
|
|
70
|
+
return (
|
|
71
|
+
<select aria-label="Choose a form" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" defaultValue="" onChange={(e) => { const f = forms.data?.[Number(e.target.value)]; if (f) setPicked({ form_key: f.form_key, source_id: f.source_id }); }}>
|
|
72
|
+
<option value="" disabled>Choose a form</option>
|
|
73
|
+
{(forms.data ?? []).map((f, i) => <option key={`${f.source_id}:${f.form_key}`} value={i}>{f.display_name || f.form_key}</option>)}
|
|
74
|
+
</select>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
// Keyed on the form: page, trace and retry state belong to one form.
|
|
78
|
+
return <SubmissionsFor key={`${form.source_id}:${form.form_key}`} form={form} pageSize={pageSize} maxFieldColumns={maxFieldColumns} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function SubmissionsFor({ form, pageSize, maxFieldColumns }: { form: FormRef; pageSize: number; maxFieldColumns: number }) {
|
|
82
|
+
const [page, setPage] = useState(1);
|
|
83
|
+
const [traceOf, setTraceOf] = useState<FormSubmission | null>(null);
|
|
84
|
+
const [retrying, setRetrying] = useState<FormSubmission | null>(null);
|
|
85
|
+
const result = useG8Query<Page>(
|
|
86
|
+
(signal) => g8Post<unknown>(`/forms/${encodeURIComponent(form.form_key)}/submissions`, { page, per_page: pageSize, source_id: form.source_id }, { signal }).then((r) => {
|
|
87
|
+
const data = r.data as { submissions?: { items?: FormSubmission[]; total?: number }; items?: FormSubmission[]; total?: number } | undefined;
|
|
88
|
+
const block = data?.submissions ?? data;
|
|
89
|
+
return { items: block?.items ?? [], total: block?.total ?? 0 };
|
|
90
|
+
}),
|
|
91
|
+
[form.form_key, form.source_id, page, pageSize],
|
|
92
|
+
);
|
|
93
|
+
const retry = useG8Mutation((s: FormSubmission) => g8Post(`/forms/deliveries/${encodeURIComponent(s.delivery?.receipt_id ?? "")}/retry`), {
|
|
94
|
+
onSuccess: () => { setRetrying(null); void result.refetch(); },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (result.error) return <ApiError error={result.error} title="Could not load submissions" onRetry={() => void result.refetch()} />;
|
|
98
|
+
if (result.loading && !result.data) return <div className="grid gap-2" aria-busy="true">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-9 w-full" />)}</div>;
|
|
99
|
+
const items = result.data?.items ?? [];
|
|
100
|
+
const total = result.data?.total ?? items.length;
|
|
101
|
+
// A page past the end (rows removed, fewer submissions than before) is not "no submissions": keep the pager so the person can go back.
|
|
102
|
+
if (items.length === 0 && total > 0 && page > 1) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="grid gap-2">
|
|
105
|
+
<EmptyState title="This page is empty" description={`There are ${total.toLocaleString()} submissions, but none on page ${page}.`} />
|
|
106
|
+
<div><Button size="sm" variant="outline" onClick={() => setPage(1)}>Back to the first page</Button></div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (items.length === 0) return <EmptyState title="No submissions yet" description="Submit the form once on your live site. Submissions appear within a minute." />;
|
|
111
|
+
|
|
112
|
+
const columns = [...new Set(items.flatMap((s) => Object.keys(s.form_data ?? {})))].slice(0, maxFieldColumns);
|
|
113
|
+
return (
|
|
114
|
+
<div className="grid gap-3">
|
|
115
|
+
<Table>
|
|
116
|
+
<TableHeader>
|
|
117
|
+
<TableRow>
|
|
118
|
+
<TableHead>Received</TableHead>
|
|
119
|
+
{columns.map((c) => <TableHead key={c}>{c}</TableHead>)}
|
|
120
|
+
<TableHead>Delivery</TableHead>
|
|
121
|
+
<TableHead />
|
|
122
|
+
</TableRow>
|
|
123
|
+
</TableHeader>
|
|
124
|
+
<TableBody>
|
|
125
|
+
{items.map((s) => (
|
|
126
|
+
<TableRow key={s.id}>
|
|
127
|
+
<TableCell>{s.received_at ? new Date(s.received_at).toLocaleString() : ""}{s.is_test ? " (test)" : ""}</TableCell>
|
|
128
|
+
{columns.map((c) => <TableCell key={c}>{cellText(s.form_data?.[c])}</TableCell>)}
|
|
129
|
+
<TableCell className={submissionFailed(s) ? "text-red-700" : undefined}>{submissionState(s)}</TableCell>
|
|
130
|
+
<TableCell className="text-right">
|
|
131
|
+
<Button size="sm" variant="ghost" onClick={() => setTraceOf(s)}>Trace</Button>
|
|
132
|
+
{submissionFailed(s) && s.delivery?.receipt_id ? <Button size="sm" variant="ghost" onClick={() => { retry.reset(); setRetrying(s); }}>Retry</Button> : null}
|
|
133
|
+
</TableCell>
|
|
134
|
+
</TableRow>
|
|
135
|
+
))}
|
|
136
|
+
</TableBody>
|
|
137
|
+
</Table>
|
|
138
|
+
<div className="flex items-center justify-between text-sm">
|
|
139
|
+
<span className="text-muted-foreground">{total.toLocaleString()} submissions. Empty identifying fields may be redacted for your key.</span>
|
|
140
|
+
<span>
|
|
141
|
+
<Button size="sm" variant="ghost" disabled={page <= 1} onClick={() => setPage(page - 1)}>Previous</Button>
|
|
142
|
+
<Button size="sm" variant="ghost" disabled={page * pageSize >= total} onClick={() => setPage(page + 1)}>Next</Button>
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
<SubmissionTrace form={form} submission={traceOf} onClose={() => setTraceOf(null)} />
|
|
146
|
+
<ConfirmSpendDialog
|
|
147
|
+
open={retrying !== null}
|
|
148
|
+
tier="external"
|
|
149
|
+
title="Retry delivering this lead?"
|
|
150
|
+
description={<><p>The lead is sent to the form's destination again, creating or updating a contact.</p>{retry.error ? <ApiError error={retry.error} title="Could not retry" /> : null}</>}
|
|
151
|
+
confirmLabel="Retry delivery"
|
|
152
|
+
confirming={retry.loading}
|
|
153
|
+
onCancel={() => { retry.reset(); setRetrying(null); }}
|
|
154
|
+
onConfirm={() => { if (retrying) void retry.mutate(retrying); }}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Types and helpers for the forms block.
|
|
5
|
+
*
|
|
6
|
+
* graph8 does not host a drag-and-drop form designer. It CAPTURES the lead forms on your site (through the
|
|
7
|
+
* tracking snippet, see FormEmbed), lists them, and ROUTES each submission to a list with a field mapping.
|
|
8
|
+
* Several responses are passed through from the app and are not fully typed in the contract, so the readers
|
|
9
|
+
* here accept the shapes the app is known to send and fall back to an empty list instead of throwing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Identifies a form: a form key is only unique within one installed source (site). */
|
|
13
|
+
export interface FormRef {
|
|
14
|
+
form_key: string;
|
|
15
|
+
source_id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FormListItem extends FormRef {
|
|
19
|
+
id: string;
|
|
20
|
+
display_name?: string | null;
|
|
21
|
+
page_url?: string | { value?: string } | null;
|
|
22
|
+
total_submissions?: number | null;
|
|
23
|
+
failed_submissions?: number | null;
|
|
24
|
+
latest_submitted_at?: string | null;
|
|
25
|
+
delivery_health?: string | null;
|
|
26
|
+
target_list_id?: number | string | null;
|
|
27
|
+
target_list_name?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FormOverview extends FormRef {
|
|
31
|
+
display_name?: string | null;
|
|
32
|
+
capture_health?: string | null;
|
|
33
|
+
delivery_health?: string | null;
|
|
34
|
+
delivery_source?: string | null;
|
|
35
|
+
delivery_warning?: string | null;
|
|
36
|
+
recommended_action?: string | null;
|
|
37
|
+
route?: { version?: number | null; mapping?: MappingRow[] | null } | null;
|
|
38
|
+
form_to_contact_field_mapping?: MappingRow[] | null;
|
|
39
|
+
destination?: { list_id?: number | null; list_name?: string | null; route_version?: number | null; source?: string | null } | null;
|
|
40
|
+
metrics?: { captured?: number; processed?: number; destination_verified?: number; failed?: number; activated?: number; unprocessed?: number | null } | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MappingRow {
|
|
44
|
+
source_field: string;
|
|
45
|
+
target_field: string;
|
|
46
|
+
static_value?: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ContactFieldGroup {
|
|
50
|
+
name: string;
|
|
51
|
+
fields: Array<{ field: string; label: string; type?: string }>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FormSubmission {
|
|
55
|
+
id: string;
|
|
56
|
+
received_at?: string;
|
|
57
|
+
form_data?: Record<string, unknown>;
|
|
58
|
+
delivery?: { state?: string; delivery_state?: string; failed?: boolean; reason?: string; receipt_id?: string } | null;
|
|
59
|
+
delivery_state?: string;
|
|
60
|
+
is_test?: boolean;
|
|
61
|
+
page?: { url?: string; path?: string } | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The first array found under any of `keys` (or the value itself if it is an array). */
|
|
65
|
+
export function pickItems<T>(data: unknown, ...keys: string[]): T[] {
|
|
66
|
+
if (Array.isArray(data)) return data as T[];
|
|
67
|
+
if (data && typeof data === "object") {
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = (data as Record<string, unknown>)[key];
|
|
70
|
+
if (Array.isArray(value)) return value as T[];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `page_url` is a string in the list and `{ value }` in the detail view. */
|
|
77
|
+
export function pageUrlText(url: FormListItem["page_url"]): string {
|
|
78
|
+
if (!url) return "";
|
|
79
|
+
return typeof url === "string" ? url : (url.value ?? "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function formLabel(form: Partial<FormListItem> & { form_key?: string }): string {
|
|
83
|
+
return form.display_name?.trim() || form.form_key || "Untitled form";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One status word per form, from the server's own health fields. */
|
|
87
|
+
export function formStatus(form: Pick<FormListItem, "delivery_health" | "target_list_id" | "total_submissions">): "failing" | "paused" | "live" | "needs_setup" | "no_data" {
|
|
88
|
+
if (form.delivery_health === "failing") return "failing";
|
|
89
|
+
if (form.delivery_health === "paused") return "paused";
|
|
90
|
+
if (form.target_list_id) return "live";
|
|
91
|
+
return (form.total_submissions ?? 0) > 0 ? "needs_setup" : "no_data";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const STATUS_LABEL: Record<ReturnType<typeof formStatus>, string> = {
|
|
95
|
+
failing: "Failing",
|
|
96
|
+
paused: "Paused",
|
|
97
|
+
live: "Live",
|
|
98
|
+
needs_setup: "Needs setup",
|
|
99
|
+
no_data: "No data",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function submissionState(s: FormSubmission): string {
|
|
103
|
+
return s.delivery?.state ?? s.delivery?.delivery_state ?? s.delivery_state ?? "not delivered";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function submissionFailed(s: FormSubmission): boolean {
|
|
107
|
+
return Boolean(s.delivery?.failed) || /fail/i.test(submissionState(s));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function cellText(value: unknown): string {
|
|
111
|
+
if (value == null) return "";
|
|
112
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The mapping body both preview and save take. Rows with no target are dropped. */
|
|
116
|
+
export function mappingPayload(source_id: string, rows: readonly MappingRow[], listName: string, listId: number | undefined, routeVersion?: number | null) {
|
|
117
|
+
return {
|
|
118
|
+
source_id,
|
|
119
|
+
target_list_name: listName,
|
|
120
|
+
...(listId ? { target_list_id: listId } : {}),
|
|
121
|
+
...(routeVersion ? { expected_route_version: routeVersion } : {}),
|
|
122
|
+
form_to_contact_field_mapping: rows
|
|
123
|
+
.filter((r) => r.target_field && (r.source_field || r.static_value))
|
|
124
|
+
// A constant has no submitted field; the API still requires a source name, so reuse the target.
|
|
125
|
+
.map((r) => ({ source_field: r.source_field || r.target_field, target_field: r.target_field, ...(r.static_value ? { static_value: r.static_value } : {}) })),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Get the tracking snippet and a lead-capture form for a site
|
|
3
|
+
* @when Wiring a site (or your Next.js layout) so graph8 captures its forms. The snippet must load on every page
|
|
4
|
+
* that has a form; the generated form is optional and already calls graph8.track('form_submitted').
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
// 1. The snippet. With several sites the API sets action_required and lists them: pick one with stream_id.
|
|
11
|
+
const snippet = await g8.api.snippet.getOrgSnippet({});
|
|
12
|
+
if (snippet.data.action_required) {
|
|
13
|
+
for (const stream of snippet.data.available_streams ?? []) console.log(stream.id, stream.name, stream.domains);
|
|
14
|
+
throw new Error("Several sites: re-run with query: { stream_id } for the one you mean.");
|
|
15
|
+
}
|
|
16
|
+
console.log("Put this in <head> on every page:", snippet.data.script_tag);
|
|
17
|
+
console.log("Or, in a React app:", snippet.data.react_snippet);
|
|
18
|
+
|
|
19
|
+
// 2. A ready-made lead form (name, work email, company). Drop `html` into a page that loads the snippet.
|
|
20
|
+
const form = await g8.api.snippet.getFormTemplate({});
|
|
21
|
+
console.log((form.data as { html?: string }).html);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Find failed lead deliveries and retry them one by one
|
|
3
|
+
* @when A form's destination was broken for a while (deleted list, paused route) and leads piled up as failed.
|
|
4
|
+
* Retry is scoped to ONE receipt and creates or updates a real contact, so cap how many you retry per run.
|
|
5
|
+
* (Replaying a whole window is a backfill, which is not on this surface.)
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
interface Submission {
|
|
12
|
+
id: string;
|
|
13
|
+
delivery?: { failed?: boolean; receipt_id?: string; reason?: string } | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function retryFailed(formKey: string, sourceId: string, max = 25): Promise<number> {
|
|
17
|
+
const res = await g8.api.forms.searchFormSubmissions({ path: { form_key: formKey }, body: { source_id: sourceId, page: 1, per_page: 100 } });
|
|
18
|
+
const data = res.data as { submissions?: { items?: Submission[] }; items?: Submission[] };
|
|
19
|
+
const items = data.submissions?.items ?? data.items ?? [];
|
|
20
|
+
const failed = items.filter((s) => s.delivery?.failed && s.delivery.receipt_id).slice(0, max);
|
|
21
|
+
for (const s of failed) {
|
|
22
|
+
console.log("retrying", s.id, s.delivery?.reason);
|
|
23
|
+
await g8.api.forms.retryFormDelivery({ path: { receipt_id: s.delivery!.receipt_id! } });
|
|
24
|
+
}
|
|
25
|
+
return failed.length;
|
|
26
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Route a captured form's leads to a list with a field mapping
|
|
3
|
+
* @when A form shows up in the inventory but its leads go nowhere ("Needs setup"). Order matters: read the
|
|
4
|
+
* fields it submits, preview the mapping, save it, then send one test submission. Preview writes nothing.
|
|
5
|
+
*/
|
|
6
|
+
import { g8, G8Error } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
// 1. Find the form. A form_key is only unique within its source_id (the site), so keep both.
|
|
11
|
+
const forms = await g8.api.forms.listForms({ query: { search: "demo request" } });
|
|
12
|
+
const form = forms.data.items?.[0];
|
|
13
|
+
if (!form?.form_key || !form.source_id) throw new Error("No captured form matches. Submit it once on the live site first.");
|
|
14
|
+
const ref = { path: { form_key: form.form_key }, query: { source_id: form.source_id } };
|
|
15
|
+
|
|
16
|
+
// 2. What the form actually submits (the mapping's left side).
|
|
17
|
+
const fields = await g8.api.forms.getFormFields(ref);
|
|
18
|
+
console.log(fields.data);
|
|
19
|
+
|
|
20
|
+
// 3. The destination list, created if you do not have one.
|
|
21
|
+
const list = await g8.lists.create("Demo requests");
|
|
22
|
+
|
|
23
|
+
// 4. Map submitted field -> contact field. Contact field names come from GET /contacts/form-fields.
|
|
24
|
+
const mapping = {
|
|
25
|
+
source_id: form.source_id,
|
|
26
|
+
target_list_id: Number(list.id),
|
|
27
|
+
target_list_name: "Demo requests",
|
|
28
|
+
form_to_contact_field_mapping: [
|
|
29
|
+
{ source_field: "email", target_field: "CONTACT_WORK_EMAIL" },
|
|
30
|
+
{ source_field: "company", target_field: "COMPANY_NAME" },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
const preview = await g8.api.forms.previewFormMapping({ path: { form_key: form.form_key }, body: mapping });
|
|
34
|
+
console.log("Preview (nothing saved):", preview.data);
|
|
35
|
+
|
|
36
|
+
// 5. Save, then prove the route works. The test adds one synthetic contact to the list.
|
|
37
|
+
try {
|
|
38
|
+
await g8.api.forms.saveFormMapping({ path: { form_key: form.form_key }, body: mapping });
|
|
39
|
+
const test = await g8.api.forms.testFormRoute({ path: { form_key: form.form_key }, body: { source_id: form.source_id } });
|
|
40
|
+
console.log("Test submission:", test.data);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err instanceof G8Error && err.status === 409) console.error("Someone else saved this route in between: re-read and retry.");
|
|
43
|
+
else throw err;
|
|
44
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# inbox: unified reply inbox
|
|
2
|
+
|
|
3
|
+
One queue for replies on email, SMS and LinkedIn: a paged list with channel and status filters, a
|
|
4
|
+
conversation view, an AI-assisted reply composer, tags and assignment. Compose the four parts yourself or
|
|
5
|
+
copy `InboxScreen`, the finished two-pane page.
|
|
6
|
+
|
|
7
|
+
Depends on: `crm` (the contact on each thread), `mailboxes` (the connected sending mailboxes). Install with
|
|
8
|
+
`npx @graph8/sdk add inbox`.
|
|
9
|
+
|
|
10
|
+
## The flow, in order
|
|
11
|
+
|
|
12
|
+
1. **List** `GET /inbox?channel=&status=&page=&page_size=` (`InboxList`). Each row has `id` and `channel`.
|
|
13
|
+
Pagination is page-number based (`pagination.has_next`), not cursor based.
|
|
14
|
+
2. **Open** `GET /inbox/{reply_id}?channel=` (`ThreadView`). Pass the row's `id` AND `channel`; ids are only
|
|
15
|
+
unique per channel. Messages with `is_draft: true` are unsent drafts.
|
|
16
|
+
3. **Draft** `POST /inbox/drafts/generate` (`ReplyComposer`, **billable**, confirm first).
|
|
17
|
+
`generate_from_scratch` + `conversation_id` + `channel_type` writes a reply grounded in the thread;
|
|
18
|
+
`shorten` / `expand` / `fix_grammar_spelling` rework the text you pass in `message_html`. The response
|
|
19
|
+
`data.content` is HTML and `data.plain_content` is text. It sends nothing.
|
|
20
|
+
4. **Send** `POST /inbox/{reply_id}/send` (`ReplyComposer`, **external**, confirm first) with
|
|
21
|
+
`{ channel, body, subject?, to? }`. The composer sends an `Idempotency-Key` so a double click cannot
|
|
22
|
+
send twice.
|
|
23
|
+
5. **Route** (`TagAndAssign`): `POST /inbox/tags` creates a tag (write), `POST /inbox/{id}/tag?channel=`
|
|
24
|
+
applies tag ids and `POST /inbox/{id}/assign?channel=` hands the thread to a teammate by email. Both of
|
|
25
|
+
those are **external** (confirm first) because they can fire automations and notify people.
|
|
26
|
+
|
|
27
|
+
IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/send/tag/assign),
|
|
28
|
+
`tag.id` (create -> apply), `user.email` from `GET /inbox/users` (assign).
|
|
29
|
+
|
|
30
|
+
## Prerequisites a human must do
|
|
31
|
+
|
|
32
|
+
- Connect a mailbox in the graph8 app (Google / Microsoft OAuth exists only there; see the `mailboxes` block).
|
|
33
|
+
Until then the inbox has no email threads. `where: "app"`.
|
|
34
|
+
- Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run`.
|
|
35
|
+
- Credits for AI drafts (402 when none; there is no balance read, so the confirm dialog shows no estimate).
|
|
36
|
+
|
|
37
|
+
## Defaults (the agent does not need to ask)
|
|
38
|
+
|
|
39
|
+
- AI drafts are only ever written into the reply box. Sending is always a separate, confirmed click.
|
|
40
|
+
- Tone `professional`, goal `follow_up`.
|
|
41
|
+
- No assignee filter: the API cannot filter by an arbitrary assignee email (an org key owns no threads), so
|
|
42
|
+
filter `thread.assignees` client-side if you need it.
|
|
43
|
+
- Ask the human only about **tags**: there is no list-tags operation. Pass `availableTags` from your own
|
|
44
|
+
state (collect them from `thread.tags` of loaded rows, or keep a list); tags created in the panel are
|
|
45
|
+
offered for the session.
|
|
46
|
+
|
|
47
|
+
## Gotchas
|
|
48
|
+
|
|
49
|
+
- `GET /inbox/{reply_id}/draft` is classified `read` but **charges credits**. Do not call it to preview;
|
|
50
|
+
this block uses `POST /inbox/drafts/generate` (billable, confirmed) instead.
|
|
51
|
+
- LinkedIn threads have no AI drafting in this block (the draft endpoint supports email and SMS channel
|
|
52
|
+
types); the composer hides the AI buttons for them and sends with the thread's own account.
|
|
53
|
+
- Tag and assign take the thread's `?channel=` query as well as the id.
|
|
54
|
+
|
|
55
|
+
## Operations
|
|
56
|
+
|
|
57
|
+
| Operation | Method and path | Tier | Used by |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `inbox.listInbox` | GET /inbox | read | InboxList, useInboxThreads |
|
|
60
|
+
| `inbox.getReply` | GET /inbox/{reply_id} | read | ThreadView, useInboxThread |
|
|
61
|
+
| `aiInbox.generateInboxDraft` | POST /inbox/drafts/generate | billable | ReplyComposer |
|
|
62
|
+
| `inbox.sendReply` | POST /inbox/{reply_id}/send | external | ReplyComposer |
|
|
63
|
+
| `aiInbox.listInboxUsers` | GET /inbox/users | read | TagAndAssign |
|
|
64
|
+
| `aiInbox.createInboxTag` | POST /inbox/tags | write | TagAndAssign |
|
|
65
|
+
| `inbox.tagReply` | POST /inbox/{reply_id}/tag | external | TagAndAssign |
|
|
66
|
+
| `inbox.assignReply` | POST /inbox/{reply_id}/assign | external | TagAndAssign |
|
|
67
|
+
|
|
68
|
+
## Recipes (server side)
|
|
69
|
+
|
|
70
|
+
- `01-triage-and-reply.ts`: list, read, AI draft (billable), send (external).
|
|
71
|
+
- `02-tag-and-assign.ts`: create a tag, apply it, assign a teammate.
|
|
72
|
+
- `03-hot-contacts-digest.ts`: read-only digest of engaging contacts and unowned threads.
|