@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,200 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "forms",
|
|
4
|
+
"title": "Forms",
|
|
5
|
+
"summary": "Capture website lead forms, route each submission to a list with a field mapping, watch submissions and retry failed deliveries.",
|
|
6
|
+
"features": [
|
|
7
|
+
"tracking snippet and lead-form embed code",
|
|
8
|
+
"forms inventory with health and counts",
|
|
9
|
+
"destination list and field mapping with preview and test",
|
|
10
|
+
"pause and resume delivery",
|
|
11
|
+
"submissions table with delivery trace and retry",
|
|
12
|
+
"site audit of uncaptured forms"
|
|
13
|
+
],
|
|
14
|
+
"dependsOn": [
|
|
15
|
+
"crm"
|
|
16
|
+
],
|
|
17
|
+
"operations": [
|
|
18
|
+
"list_forms_forms_get",
|
|
19
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
20
|
+
"update_form_registry_forms__form_key__patch",
|
|
21
|
+
"pause_form_delivery_forms__form_key__delivery_pause_post",
|
|
22
|
+
"resume_form_delivery_forms__form_key__delivery_resume_post",
|
|
23
|
+
"get_form_fields_forms__form_key__fields_get",
|
|
24
|
+
"get_contact_form_fields_contacts_form_fields_get",
|
|
25
|
+
"list_lists_lists_get",
|
|
26
|
+
"get_form_route_recommendations_forms__form_key__route_recommendations_get",
|
|
27
|
+
"preview_form_mapping_forms__form_key__mapping_preview_post",
|
|
28
|
+
"save_form_mapping_forms__form_key__mapping_save_post",
|
|
29
|
+
"test_form_route_forms__form_key__route_test_post",
|
|
30
|
+
"get_org_snippet_snippet_get",
|
|
31
|
+
"get_form_template_snippet_form_get",
|
|
32
|
+
"search_form_submissions_forms__form_key__submissions_post",
|
|
33
|
+
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
34
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
35
|
+
"get_site_audit_forms_site_audit_get",
|
|
36
|
+
"request_site_audit_scan_forms_site_audit_scan_post",
|
|
37
|
+
"update_site_audit_finding_forms_site_audit_findings__finding_id__patch"
|
|
38
|
+
],
|
|
39
|
+
"components": [
|
|
40
|
+
{
|
|
41
|
+
"name": "FormBuilder",
|
|
42
|
+
"file": "components/form-builder.tsx",
|
|
43
|
+
"purpose": "Forms workspace: list captured forms, rename, pause/resume delivery, set destination and mapping, view submissions.",
|
|
44
|
+
"operations": [
|
|
45
|
+
"list_forms_forms_get",
|
|
46
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
47
|
+
"update_form_registry_forms__form_key__patch",
|
|
48
|
+
"pause_form_delivery_forms__form_key__delivery_pause_post",
|
|
49
|
+
"resume_form_delivery_forms__form_key__delivery_resume_post"
|
|
50
|
+
],
|
|
51
|
+
"tiers": [
|
|
52
|
+
"read",
|
|
53
|
+
"write",
|
|
54
|
+
"external"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "FormMappingEditor",
|
|
59
|
+
"file": "components/form-mapping-editor.tsx",
|
|
60
|
+
"purpose": "Pick the destination list and map submitted fields to contact fields, with suggest, preview, save and test.",
|
|
61
|
+
"operations": [
|
|
62
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
63
|
+
"get_form_fields_forms__form_key__fields_get",
|
|
64
|
+
"get_contact_form_fields_contacts_form_fields_get",
|
|
65
|
+
"list_lists_lists_get",
|
|
66
|
+
"get_form_route_recommendations_forms__form_key__route_recommendations_get",
|
|
67
|
+
"preview_form_mapping_forms__form_key__mapping_preview_post",
|
|
68
|
+
"save_form_mapping_forms__form_key__mapping_save_post",
|
|
69
|
+
"test_form_route_forms__form_key__route_test_post"
|
|
70
|
+
],
|
|
71
|
+
"tiers": [
|
|
72
|
+
"read",
|
|
73
|
+
"write"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "FormEmbed",
|
|
78
|
+
"file": "components/form-embed.tsx",
|
|
79
|
+
"purpose": "Copyable tracking snippet (script tag, React) and a ready-made lead-capture form with live preview.",
|
|
80
|
+
"operations": [
|
|
81
|
+
"get_org_snippet_snippet_get",
|
|
82
|
+
"get_form_template_snippet_form_get"
|
|
83
|
+
],
|
|
84
|
+
"tiers": [
|
|
85
|
+
"read"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "SubmissionsTable",
|
|
90
|
+
"file": "components/submissions-table.tsx",
|
|
91
|
+
"purpose": "Paged submissions with delivery state, a per-lead trace and a confirmed retry for failed deliveries.",
|
|
92
|
+
"operations": [
|
|
93
|
+
"search_form_submissions_forms__form_key__submissions_post",
|
|
94
|
+
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
95
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post"
|
|
96
|
+
],
|
|
97
|
+
"tiers": [
|
|
98
|
+
"read",
|
|
99
|
+
"external"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "SiteAuditPanel",
|
|
104
|
+
"file": "components/site-audit-panel.tsx",
|
|
105
|
+
"purpose": "Forms on your site that are not captured, from the latest crawl; rescan and triage findings.",
|
|
106
|
+
"operations": [
|
|
107
|
+
"get_site_audit_forms_site_audit_get",
|
|
108
|
+
"request_site_audit_scan_forms_site_audit_scan_post",
|
|
109
|
+
"update_site_audit_finding_forms_site_audit_findings__finding_id__patch"
|
|
110
|
+
],
|
|
111
|
+
"tiers": [
|
|
112
|
+
"read",
|
|
113
|
+
"write"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"hooks": [],
|
|
118
|
+
"lib": [
|
|
119
|
+
{
|
|
120
|
+
"file": "lib/form-types.ts",
|
|
121
|
+
"purpose": "Form/submission types and tolerant readers for passthrough responses, status labels, mapping payload builder."
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"recipes": [
|
|
125
|
+
{
|
|
126
|
+
"file": "recipes/install-snippet-and-form.ts",
|
|
127
|
+
"title": "Get the tracking snippet and a lead-capture form",
|
|
128
|
+
"when": "Wiring a site so graph8 captures its forms."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"file": "recipes/route-form-to-list.ts",
|
|
132
|
+
"title": "Route a captured form's leads to a list with a field mapping",
|
|
133
|
+
"when": "A form appears in the inventory but its leads go nowhere (Needs setup)."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"file": "recipes/retry-failed-deliveries.ts",
|
|
137
|
+
"title": "Find failed lead deliveries and retry them",
|
|
138
|
+
"when": "A destination was broken for a while and leads piled up as failed."
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
"prerequisites": [
|
|
142
|
+
{
|
|
143
|
+
"id": "snippet-installed",
|
|
144
|
+
"description": "The graph8 tracking snippet must be loaded on every page that has a form. Forms only appear after their first captured submission. FormEmbed gives the code; the human (or the agent, in the site's code) pastes it.",
|
|
145
|
+
"where": "app",
|
|
146
|
+
"verify": "Submit a form on the live site, then GET /api/v1/forms lists it within a minute."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"id": "destination-list",
|
|
150
|
+
"description": "A list to receive leads. Create one in the graph8 workspace (or with the lists block / recipe) before saving a mapping.",
|
|
151
|
+
"where": "api",
|
|
152
|
+
"verify": "GET /api/v1/lists returns at least one list."
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "proxy-allowlist",
|
|
156
|
+
"description": "Add \"forms\", \"snippet\", \"contacts\" and \"lists\" to allowedPrefixes in app/api/g8/[...path]/route.ts.",
|
|
157
|
+
"where": "app",
|
|
158
|
+
"verify": "The route file's allowedPrefixes contains those four prefixes."
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "signals-write",
|
|
162
|
+
"description": "Submitted values (emails, names) are redacted for keys without the Signals write capability. Use a key that has it if the app must show the leads' contact details.",
|
|
163
|
+
"where": "api",
|
|
164
|
+
"verify": "A submission's identifying fields are not empty in SubmissionsTable."
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"scopes": [
|
|
168
|
+
"contacts:read",
|
|
169
|
+
"forms:read",
|
|
170
|
+
"forms:run",
|
|
171
|
+
"forms:write",
|
|
172
|
+
"integrations:read",
|
|
173
|
+
"lists:read"
|
|
174
|
+
],
|
|
175
|
+
"defaults": [
|
|
176
|
+
{
|
|
177
|
+
"decision": "Hosted form designer vs capture existing forms",
|
|
178
|
+
"default": "Capture the site's existing forms through the snippet; the generated lead form (FormEmbed) is only a starting point, graph8 has no form designer API",
|
|
179
|
+
"askHuman": false
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"decision": "Which list receives a form's leads",
|
|
183
|
+
"default": "A new list named after the form",
|
|
184
|
+
"askHuman": true
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"decision": "Mapping when the form has an email field",
|
|
188
|
+
"default": "Map it to CONTACT_WORK_EMAIL; use Suggest mapping for the rest",
|
|
189
|
+
"askHuman": false
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"decision": "Retrying failed deliveries",
|
|
193
|
+
"default": "One lead at a time, after the person confirms (it creates or updates a contact)",
|
|
194
|
+
"askHuman": false
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
"peerDependencies": {
|
|
198
|
+
"react": ">=18"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormBuilder: the forms workspace. Pick a captured form, name it, route its leads, and watch what arrives.
|
|
5
|
+
*
|
|
6
|
+
* graph8 captures the lead forms already on your site (install the snippet with FormEmbed) rather than hosting
|
|
7
|
+
* a form designer, so "building" a form here means setting up where its submissions go and how fields map.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listForms (GET /forms?search=&page=&per_page=) read
|
|
11
|
+
* 2. getFormOverview (GET /forms/{form_key}/overview?source_id=) read
|
|
12
|
+
* 3. updateFormRegistry (PATCH /forms/{form_key}) write (rename)
|
|
13
|
+
* 4. pauseFormDelivery / resumeFormDelivery (POST .../delivery/pause|resume) external -> ConfirmSpendDialog
|
|
14
|
+
* 5. FormMappingEditor (destination list + field mapping) and SubmissionsTable (what arrived)
|
|
15
|
+
* Pausing keeps accepting submissions (they queue); resuming starts delivering to the list again, which creates
|
|
16
|
+
* and updates contacts, so both go through the confirm dialog.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { STATUS_LABEL, formLabel, formStatus, pageUrlText, pickItems } from "../lib/form-types";
|
|
28
|
+
import type { FormListItem, FormOverview, FormRef } from "../lib/form-types";
|
|
29
|
+
import { FormMappingEditor } from "./form-mapping-editor";
|
|
30
|
+
import { SubmissionsTable } from "./submissions-table";
|
|
31
|
+
|
|
32
|
+
export interface FormBuilderProps {
|
|
33
|
+
/** Form to open first. */
|
|
34
|
+
initialForm?: FormRef;
|
|
35
|
+
onSelectForm?: (form: FormListItem) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PER_PAGE = 20;
|
|
39
|
+
|
|
40
|
+
function FormHeader({ form, onChanged }: { form: FormListItem; onChanged: () => void }) {
|
|
41
|
+
const base = `/forms/${encodeURIComponent(form.form_key)}`;
|
|
42
|
+
const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, { source_id: form.source_id }, { signal }).then((r) => r.data), [base, form.source_id]);
|
|
43
|
+
const [name, setName] = useState(form.display_name ?? "");
|
|
44
|
+
const [confirm, setConfirm] = useState<"pause" | "resume" | null>(null);
|
|
45
|
+
const rename = useG8Mutation(() => g8Patch(base, { source_id: form.source_id, display_name: name.trim() }), { onSuccess: onChanged });
|
|
46
|
+
const toggle = useG8Mutation((action: "pause" | "resume") => g8Post(`${base}/delivery/${action}`, { source_id: form.source_id }), {
|
|
47
|
+
onSuccess: () => { setConfirm(null); void overview.refetch(); onChanged(); },
|
|
48
|
+
});
|
|
49
|
+
const paused = overview.data?.delivery_health === "paused";
|
|
50
|
+
const m = overview.data?.metrics;
|
|
51
|
+
return (
|
|
52
|
+
<div className="grid gap-3">
|
|
53
|
+
<form className="flex items-end gap-2" onSubmit={(e) => { e.preventDefault(); if (name.trim()) void rename.mutate(undefined); }}>
|
|
54
|
+
<div className="max-w-sm flex-1">
|
|
55
|
+
<label htmlFor="g8-form-name" className="text-muted-foreground text-xs">Form name</label>
|
|
56
|
+
<Input id="g8-form-name" value={name} placeholder={form.form_key} onChange={(e) => setName(e.target.value)} />
|
|
57
|
+
</div>
|
|
58
|
+
<Button type="submit" size="sm" variant="outline" loading={rename.loading} disabled={!name.trim() || name.trim() === form.display_name}>Rename</Button>
|
|
59
|
+
<Button type="button" size="sm" variant="outline" disabled={!overview.data} onClick={() => setConfirm(paused ? "resume" : "pause")}>{paused ? "Resume delivery" : "Pause delivery"}</Button>
|
|
60
|
+
</form>
|
|
61
|
+
{rename.error ? <ApiError error={rename.error} title="Could not rename the form" /> : null}
|
|
62
|
+
{overview.error ? <ApiError error={overview.error} title="Could not load the form overview" onRetry={() => void overview.refetch()} /> : null}
|
|
63
|
+
{m ? (
|
|
64
|
+
<dl className="grid grid-cols-2 gap-2 text-sm sm:grid-cols-5">
|
|
65
|
+
{([["Captured", m.captured], ["Processed", m.processed], ["In destination", m.destination_verified], ["Failed", m.failed], ["Activated", m.activated]] as const).map(([label, value]) => (
|
|
66
|
+
<div key={label}><dt className="text-muted-foreground text-xs">{label}</dt><dd className="font-medium">{value ?? 0}</dd></div>
|
|
67
|
+
))}
|
|
68
|
+
</dl>
|
|
69
|
+
) : null}
|
|
70
|
+
{overview.data?.delivery_warning ? <p className="text-sm text-amber-700">{overview.data.delivery_warning}</p> : null}
|
|
71
|
+
<ConfirmSpendDialog
|
|
72
|
+
open={confirm !== null}
|
|
73
|
+
tier="external"
|
|
74
|
+
title={confirm === "pause" ? "Pause delivery for this form?" : "Resume delivery for this form?"}
|
|
75
|
+
confirmLabel={confirm === "pause" ? "Pause" : "Resume"}
|
|
76
|
+
description={<><p>{confirm === "pause" ? "New submissions are still captured but queue instead of reaching your list." : "Queued and new submissions are delivered to the destination list, creating or updating contacts."}</p>{toggle.error ? <ApiError error={toggle.error} title="Could not change delivery" /> : null}</>}
|
|
77
|
+
confirming={toggle.loading}
|
|
78
|
+
onCancel={() => setConfirm(null)}
|
|
79
|
+
onConfirm={() => { if (confirm) void toggle.mutate(confirm); }}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function FormBuilder({ initialForm, onSelectForm }: FormBuilderProps) {
|
|
86
|
+
const [page, setPage] = useState(1);
|
|
87
|
+
const [searchText, setSearchText] = useState("");
|
|
88
|
+
const [search, setSearch] = useState("");
|
|
89
|
+
const [selected, setSelected] = useState<FormRef | undefined>(initialForm);
|
|
90
|
+
const [view, setView] = useState<"setup" | "submissions">("setup");
|
|
91
|
+
const forms = useG8Query(
|
|
92
|
+
(signal) => g8Get<{ items?: FormListItem[]; total?: number }>("/forms", { page, per_page: PER_PAGE, search: search || undefined }, { signal }).then((r) => ({ items: pickItems<FormListItem>(r.data, "items"), total: (r.data as { total?: number } | undefined)?.total ?? 0 })),
|
|
93
|
+
[page, search],
|
|
94
|
+
);
|
|
95
|
+
const items = forms.data?.items ?? [];
|
|
96
|
+
const [lastItem, setLastItem] = useState<FormListItem | undefined>(undefined);
|
|
97
|
+
const onPage = items.find((f) => f.form_key === selected?.form_key && f.source_id === selected?.source_id);
|
|
98
|
+
// A selected form can be off this page (initialForm, or filtered out by a search): keep working with it.
|
|
99
|
+
const current: FormListItem | undefined = onPage ?? (selected ? (lastItem?.form_key === selected.form_key && lastItem.source_id === selected.source_id ? lastItem : { id: `${selected.source_id}:${selected.form_key}`, ...selected }) : undefined);
|
|
100
|
+
|
|
101
|
+
if (forms.error) return <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} />;
|
|
102
|
+
return (
|
|
103
|
+
<div className="grid gap-4 md:grid-cols-[20rem_1fr]">
|
|
104
|
+
<Card>
|
|
105
|
+
<CardHeader><CardTitle>Forms</CardTitle></CardHeader>
|
|
106
|
+
<CardContent className="grid gap-3">
|
|
107
|
+
<form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); setPage(1); setSearch(searchText.trim()); }}>
|
|
108
|
+
<Input aria-label="Search forms" placeholder="Search forms" value={searchText} onChange={(e) => setSearchText(e.target.value)} />
|
|
109
|
+
<Button type="submit" size="sm" variant="outline">Search</Button>
|
|
110
|
+
</form>
|
|
111
|
+
{forms.loading && !forms.data ? <div className="grid gap-2" aria-busy="true"><Skeleton className="h-12 w-full" /><Skeleton className="h-12 w-full" /></div> : items.length === 0 ? (
|
|
112
|
+
<EmptyState title={search ? "No forms match" : "No forms captured yet"} description={search ? "Try another name." : "Install the graph8 snippet on your site (see FormEmbed). Forms appear here after their first submission."} />
|
|
113
|
+
) : (
|
|
114
|
+
<ul className="grid gap-1">
|
|
115
|
+
{items.map((f) => {
|
|
116
|
+
const status = formStatus(f);
|
|
117
|
+
return (
|
|
118
|
+
<li key={`${f.source_id}:${f.form_key}`}>
|
|
119
|
+
<Button variant={f === current ? "secondary" : "ghost"} className="h-auto w-full flex-col items-start gap-0 py-2 text-left" onClick={() => { setSelected({ form_key: f.form_key, source_id: f.source_id }); setLastItem(f); onSelectForm?.(f); }}>
|
|
120
|
+
<span className="font-medium">{formLabel(f)}</span>
|
|
121
|
+
<span className="text-muted-foreground text-xs">{STATUS_LABEL[status]} · {f.total_submissions ?? 0} submissions{f.failed_submissions ? ` · ${f.failed_submissions} failed` : ""}</span>
|
|
122
|
+
<span className="text-muted-foreground max-w-full truncate text-xs">{pageUrlText(f.page_url)}</span>
|
|
123
|
+
</Button>
|
|
124
|
+
</li>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</ul>
|
|
128
|
+
)}
|
|
129
|
+
<div className="flex items-center justify-between text-sm">
|
|
130
|
+
<Button size="sm" variant="ghost" disabled={page <= 1} onClick={() => setPage(page - 1)}>Previous</Button>
|
|
131
|
+
<span className="text-muted-foreground">Page {page}</span>
|
|
132
|
+
<Button size="sm" variant="ghost" disabled={page * PER_PAGE >= (forms.data?.total ?? 0)} onClick={() => setPage(page + 1)}>Next</Button>
|
|
133
|
+
</div>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
<div className="grid content-start gap-4">
|
|
137
|
+
{!selected || !current ? <EmptyState title="Pick a form" description="Choose a form to set its destination list and field mapping, or to see its submissions." /> : (
|
|
138
|
+
<>
|
|
139
|
+
<FormHeader key={`${current.source_id}:${current.form_key}`} form={current} onChanged={() => void forms.refetch()} />
|
|
140
|
+
<div className="flex gap-2" role="group" aria-label="Form sections">
|
|
141
|
+
<Button size="sm" variant={view === "setup" ? "secondary" : "ghost"} onClick={() => setView("setup")}>Setup</Button>
|
|
142
|
+
<Button size="sm" variant={view === "submissions" ? "secondary" : "ghost"} onClick={() => setView("submissions")}>Submissions</Button>
|
|
143
|
+
</div>
|
|
144
|
+
{view === "setup" ? <FormMappingEditor key={`${current.source_id}:${current.form_key}`} form={current} onSaved={() => void forms.refetch()} /> : <SubmissionsTable form={current} />}
|
|
145
|
+
</>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormEmbed: the code to put on your site so graph8 captures forms, plus a ready-made lead-capture form.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getOrgSnippet (GET /snippet?stream_id=) read (write key, <script> tag, React component, allowed domains)
|
|
8
|
+
* 2. getFormTemplate (GET /snippet/form?stream_id=) read (a self-contained HTML lead form that calls graph8.track)
|
|
9
|
+
* Both are copy-and-paste outputs; nothing is installed for you. The snippet must load on every page that has a form.
|
|
10
|
+
* With several streams (sites) the API answers `action_required` and lists them: the picker below chooses one.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Label, Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
18
|
+
|
|
19
|
+
export interface FormEmbedProps {
|
|
20
|
+
/** Stream (site) to read the snippet for. Omit for the organization's main stream. */
|
|
21
|
+
streamId?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Snippet {
|
|
25
|
+
write_key: string;
|
|
26
|
+
script_tag: string;
|
|
27
|
+
react_snippet: string;
|
|
28
|
+
domains?: string[];
|
|
29
|
+
stream_id?: string;
|
|
30
|
+
action_required?: boolean;
|
|
31
|
+
warning?: string | null;
|
|
32
|
+
available_streams?: Array<{ id: string; name: string; domains?: string[] }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CodeBlock({ label, code }: { label: string; code: string }) {
|
|
36
|
+
const [copied, setCopied] = useState(false);
|
|
37
|
+
const copy = async (): Promise<void> => {
|
|
38
|
+
try {
|
|
39
|
+
await navigator.clipboard.writeText(code);
|
|
40
|
+
setCopied(true);
|
|
41
|
+
setTimeout(() => setCopied(false), 1500);
|
|
42
|
+
} catch {
|
|
43
|
+
setCopied(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return (
|
|
47
|
+
<div className="grid gap-1">
|
|
48
|
+
<div className="flex items-center justify-between">
|
|
49
|
+
<Label>{label}</Label>
|
|
50
|
+
<Button size="sm" variant="outline" onClick={() => void copy()}>{copied ? "Copied" : `Copy ${label.toLowerCase()}`}</Button>
|
|
51
|
+
</div>
|
|
52
|
+
<pre className="bg-muted max-h-64 overflow-auto rounded-md p-3 text-xs" aria-label={label}>{code}</pre>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function FormEmbed({ streamId }: FormEmbedProps) {
|
|
58
|
+
const [stream, setStream] = useState<string | undefined>(streamId);
|
|
59
|
+
const snippet = useG8Query((signal) => g8Get<Snippet>("/snippet", { stream_id: stream }, { signal }).then((r) => r.data), [stream]);
|
|
60
|
+
const template = useG8Query((signal) => g8Get<{ html?: string }>("/snippet/form", { stream_id: stream }, { signal }).then((r) => r.data), [stream]);
|
|
61
|
+
const [showPreview, setShowPreview] = useState(false);
|
|
62
|
+
|
|
63
|
+
if (snippet.error) return <ApiError error={snippet.error} title="Could not load the tracking snippet" onRetry={() => void snippet.refetch()} />;
|
|
64
|
+
if (snippet.loading && !snippet.data) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-48" /><Skeleton className="h-32 w-full" /></div>;
|
|
65
|
+
const s = snippet.data;
|
|
66
|
+
const html = template.data?.html ?? "";
|
|
67
|
+
return (
|
|
68
|
+
<div className="grid gap-4">
|
|
69
|
+
{s?.action_required && s.available_streams?.length ? (
|
|
70
|
+
<Card>
|
|
71
|
+
<CardContent className="grid gap-2 pt-6">
|
|
72
|
+
<Label htmlFor="g8-stream">Which site is this for?</Label>
|
|
73
|
+
<select id="g8-stream" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" value={stream ?? s.stream_id ?? ""} onChange={(e) => setStream(e.target.value)}>
|
|
74
|
+
{s.available_streams.map((st) => <option key={st.id} value={st.id}>{st.name}{st.domains?.length ? ` (${st.domains.join(", ")})` : ""}</option>)}
|
|
75
|
+
</select>
|
|
76
|
+
{s.warning ? <p className="text-sm text-amber-700">{s.warning}</p> : null}
|
|
77
|
+
</CardContent>
|
|
78
|
+
</Card>
|
|
79
|
+
) : null}
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader><CardTitle>1. Install the tracking snippet</CardTitle></CardHeader>
|
|
82
|
+
<CardContent className="grid gap-3">
|
|
83
|
+
<p className="text-muted-foreground text-sm">Put this on every page that has a form. graph8 then detects the forms and records submissions. Allowed domains: {s?.domains?.join(", ") || "any"}.</p>
|
|
84
|
+
{s?.script_tag ? <CodeBlock label="Script tag" code={s.script_tag} /> : null}
|
|
85
|
+
{s?.react_snippet ? <CodeBlock label="React component" code={s.react_snippet} /> : null}
|
|
86
|
+
<p className="text-muted-foreground text-xs">Write key: <code>{s?.write_key}</code> (public, safe in page source).</p>
|
|
87
|
+
</CardContent>
|
|
88
|
+
</Card>
|
|
89
|
+
<Card>
|
|
90
|
+
<CardHeader><CardTitle>2. Optional: a ready-made lead form</CardTitle></CardHeader>
|
|
91
|
+
<CardContent className="grid gap-3">
|
|
92
|
+
{template.error ? <ApiError error={template.error} title="Could not load the form template" onRetry={() => void template.refetch()} /> : template.loading && !template.data ? <Skeleton className="h-24 w-full" /> : html ? (
|
|
93
|
+
<>
|
|
94
|
+
<CodeBlock label="Form HTML" code={html} />
|
|
95
|
+
<div><Button size="sm" variant="outline" onClick={() => setShowPreview((v) => !v)}>{showPreview ? "Hide preview" : "Preview the form"}</Button></div>
|
|
96
|
+
{showPreview ? <iframe title="Form preview" sandbox="" srcDoc={html} className="h-72 w-full rounded-md border" /> : null}
|
|
97
|
+
</>
|
|
98
|
+
) : <p className="text-muted-foreground text-sm">No template is available for this site.</p>}
|
|
99
|
+
</CardContent>
|
|
100
|
+
</Card>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormMappingEditor: where a form's submissions go and how its fields become contact fields.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getFormOverview (GET /forms/{form_key}/overview?source_id=) read (current destination + route version)
|
|
8
|
+
* 2. getFormFields (GET /forms/{form_key}/fields?source_id=) read (what the mapping maps FROM)
|
|
9
|
+
* 3. getContactFormFields (GET /contacts/form-fields) read (what it maps TO, grouped)
|
|
10
|
+
* 4. listLists (GET /lists) read (the destination list)
|
|
11
|
+
* 5. getFormRouteRecommendations (GET /forms/{form_key}/route/recommendations) read ("Suggest mapping")
|
|
12
|
+
* 6. previewFormMapping (POST /forms/{form_key}/mapping/preview) read (nothing is written)
|
|
13
|
+
* 7. saveFormMapping (POST /forms/{form_key}/mapping/save) write (FULL replacement; sends expected_route_version)
|
|
14
|
+
* 8. testFormRoute (POST /forms/{form_key}/route/test) write (adds ONE synthetic test contact to the list)
|
|
15
|
+
* Preview before you save: a saved mapping applies to every submission captured from then on.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useState } from "react";
|
|
19
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { cellText, mappingPayload, pickItems } from "../lib/form-types";
|
|
25
|
+
import type { ContactFieldGroup, FormOverview, FormRef, MappingRow } from "../lib/form-types";
|
|
26
|
+
|
|
27
|
+
export interface FormMappingEditorProps {
|
|
28
|
+
form: FormRef;
|
|
29
|
+
onSaved?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ListOption { id: number; title: string }
|
|
33
|
+
|
|
34
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
35
|
+
|
|
36
|
+
/** A different form needs its own mapping state, so the editor is remounted per form (key). */
|
|
37
|
+
export function FormMappingEditor(props: FormMappingEditorProps) {
|
|
38
|
+
return <FormMappingEditorFor key={`${props.form.source_id}:${props.form.form_key}`} {...props} />;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function FormMappingEditorFor({ form, onSaved }: FormMappingEditorProps) {
|
|
42
|
+
const base = `/forms/${encodeURIComponent(form.form_key)}`;
|
|
43
|
+
const q = { source_id: form.source_id };
|
|
44
|
+
const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, q, { signal }).then((r) => r.data), [base, form.source_id]);
|
|
45
|
+
const fields = useG8Query((signal) => g8Get(`${base}/fields`, q, { signal }).then((r) => pickItems<{ name: string }>(r.data, "items", "fields")), [base, form.source_id]);
|
|
46
|
+
const targets = useG8Query((signal) => g8Get<{ groups?: ContactFieldGroup[] }>("/contacts/form-fields", {}, { signal }).then((r) => r.data?.groups ?? []), []);
|
|
47
|
+
const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
|
|
48
|
+
const [rows, setRows] = useState<MappingRow[]>([]);
|
|
49
|
+
const [listId, setListId] = useState<number | undefined>(undefined);
|
|
50
|
+
const [seeded, setSeeded] = useState(false);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (seeded || !overview.data || !fields.data) return;
|
|
54
|
+
const existing = overview.data.route?.mapping ?? overview.data.form_to_contact_field_mapping ?? [];
|
|
55
|
+
// Save is a FULL replacement: keep every saved row and add a blank row for each captured field that has none yet
|
|
56
|
+
// (a site can add a field to an already-routed form).
|
|
57
|
+
const known = new Set(existing.map((m) => m.source_field));
|
|
58
|
+
setRows([...existing.map((m) => ({ ...m })), ...fields.data.filter((f) => !known.has(f.name)).map((f) => ({ source_field: f.name, target_field: "" }))]);
|
|
59
|
+
setListId(overview.data.destination?.list_id ?? undefined);
|
|
60
|
+
setSeeded(true);
|
|
61
|
+
}, [overview.data, fields.data, seeded]);
|
|
62
|
+
|
|
63
|
+
const listName = (lists.data ?? []).find((l) => l.id === listId)?.title ?? overview.data?.destination?.list_name ?? "";
|
|
64
|
+
const body = mappingPayload(form.source_id, rows, listName, listId, overview.data?.route?.version ?? overview.data?.destination?.route_version);
|
|
65
|
+
const ready = Boolean(listId) && body.form_to_contact_field_mapping.length > 0;
|
|
66
|
+
|
|
67
|
+
const suggest = useG8Mutation(async () => {
|
|
68
|
+
const r = await g8Get(`${base}/route/recommendations`, { ...q, target_list_id: listId }, {});
|
|
69
|
+
const recs = pickItems<{ source_field: string; target_field: string }>(r.data, "recommendations", "items", "mapping_recommendations");
|
|
70
|
+
setRows((prev) => prev.map((row) => (row.target_field ? row : { ...row, target_field: recs.find((x) => x.source_field === row.source_field)?.target_field ?? "" })));
|
|
71
|
+
return recs.length;
|
|
72
|
+
});
|
|
73
|
+
const preview = useG8Mutation(async () => (await g8Post(`${base}/mapping/preview`, body)).data);
|
|
74
|
+
const save = useG8Mutation(async () => (await g8Post(`${base}/mapping/save`, body)).data, { onSuccess: () => { void overview.refetch(); onSaved?.(); } });
|
|
75
|
+
const test = useG8Mutation(async () => (await g8Post(`${base}/route/test`, { source_id: form.source_id })).data);
|
|
76
|
+
|
|
77
|
+
const set = (i: number, patch: Partial<MappingRow>): void => setRows((prev) => prev.map((r, at) => (at === i ? { ...r, ...patch } : r)));
|
|
78
|
+
const loadError = overview.error ?? fields.error ?? targets.error ?? lists.error;
|
|
79
|
+
if (loadError) return <ApiError error={loadError} title="Could not load the form setup" onRetry={() => { void overview.refetch(); void fields.refetch(); }} />;
|
|
80
|
+
if (!seeded) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-9 w-64" /><Skeleton className="h-32 w-full" /></div>;
|
|
81
|
+
|
|
82
|
+
const previewRows = pickItems<Record<string, unknown>>(preview.data, "items", "rows", "preview", "submissions").slice(0, 3);
|
|
83
|
+
return (
|
|
84
|
+
<Card>
|
|
85
|
+
<CardHeader><CardTitle>Destination and field mapping</CardTitle></CardHeader>
|
|
86
|
+
<CardContent className="grid gap-4">
|
|
87
|
+
<div className="max-w-sm">
|
|
88
|
+
<Label htmlFor="g8-form-list" className="mb-1">Save leads to list</Label>
|
|
89
|
+
<select id="g8-form-list" className={SELECT} value={listId ?? ""} onChange={(e) => setListId(e.target.value ? Number(e.target.value) : undefined)}>
|
|
90
|
+
<option value="">Choose a list...</option>
|
|
91
|
+
{(lists.data ?? []).map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
|
|
92
|
+
</select>
|
|
93
|
+
</div>
|
|
94
|
+
{rows.length === 0 ? <p className="text-muted-foreground text-sm">No fields have been captured for this form yet. Submit it once on your site, then reload.</p> : (
|
|
95
|
+
<div className="grid gap-2">
|
|
96
|
+
{rows.map((row, i) => (
|
|
97
|
+
<div key={`${row.source_field}-${i}`} className="grid items-center gap-2 sm:grid-cols-[1fr_1fr_1fr]">
|
|
98
|
+
<span className="truncate text-sm font-mono">{row.source_field || "(constant)"}</span>
|
|
99
|
+
<select aria-label={`Contact field for ${row.source_field}`} className={SELECT} value={row.target_field} onChange={(e) => set(i, { target_field: e.target.value })}>
|
|
100
|
+
<option value="">Do not save</option>
|
|
101
|
+
{(targets.data ?? []).map((g) => (
|
|
102
|
+
<optgroup key={g.name} label={g.name}>{g.fields.map((f) => <option key={f.field} value={f.field}>{f.label}</option>)}</optgroup>
|
|
103
|
+
))}
|
|
104
|
+
</select>
|
|
105
|
+
<Input aria-label={`Constant for ${row.source_field}`} placeholder="Optional constant value" value={row.static_value ?? ""} onChange={(e) => set(i, { static_value: e.target.value })} />
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
<div className="flex flex-wrap gap-2">
|
|
111
|
+
<Button variant="outline" size="sm" loading={suggest.loading} disabled={!listId} onClick={() => void suggest.mutate(undefined)}>Suggest mapping</Button>
|
|
112
|
+
<Button variant="outline" size="sm" loading={preview.loading} disabled={!ready} onClick={() => void preview.mutate(undefined)}>Preview</Button>
|
|
113
|
+
<Button size="sm" loading={save.loading} disabled={!ready} onClick={() => void save.mutate(undefined)}>Save mapping</Button>
|
|
114
|
+
<Button variant="ghost" size="sm" loading={test.loading} disabled={!overview.data?.destination?.list_id} onClick={() => void test.mutate(undefined)}>Send a test submission</Button>
|
|
115
|
+
</div>
|
|
116
|
+
{[suggest.error, preview.error, save.error, test.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
|
|
117
|
+
{save.data ? <p role="status" className="text-sm text-green-700">Mapping saved. New submissions follow it.</p> : null}
|
|
118
|
+
{test.data ? <p role="status" className="text-sm">Test submission sent to the list (one synthetic contact was added). {cellText(test.data).slice(0, 200)}</p> : null}
|
|
119
|
+
{previewRows.length > 0 ? (
|
|
120
|
+
<div className="rounded border p-3 text-xs"><p className="mb-1 font-medium">Preview (nothing saved)</p>{previewRows.map((r, i) => <pre key={i} className="overflow-x-auto">{JSON.stringify(r)}</pre>)}</div>
|
|
121
|
+
) : preview.data ? <pre className="overflow-x-auto rounded border p-3 text-xs">{JSON.stringify(preview.data, null, 2).slice(0, 1200)}</pre> : null}
|
|
122
|
+
</CardContent>
|
|
123
|
+
</Card>
|
|
124
|
+
);
|
|
125
|
+
}
|