@graph8/sdk 0.361.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnrichmentJobDetail: one bulk enrichment job in full. Live status (polled with backoff, there are no streams),
|
|
5
|
+
* progress, how many records were enriched or failed, credits used, warnings, the provider errors that explain a
|
|
6
|
+
* failure, and Cancel while it runs.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getEnrichmentJob GET /enrichment/jobs/{job_id} (read) polled with useJobPoll until a final status;
|
|
10
|
+
* read once more with ?include_provider_errors=true when nothing was enriched or some records failed
|
|
11
|
+
* -> { status, total, completed, successful_enrichments, failed_enrichments, total_credits_used, warnings, provider_errors }
|
|
12
|
+
* 2. cancelEnrichmentJob POST /enrichment/jobs/{job_id}/cancel (write, free; records already done keep their data and charge)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Button } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
19
|
+
import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
23
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { isJobTerminal } from "../lib/enrichment-types";
|
|
25
|
+
import type { EnrichJob } from "../lib/enrichment-types";
|
|
26
|
+
|
|
27
|
+
export interface EnrichmentJobDetailProps {
|
|
28
|
+
jobId: string;
|
|
29
|
+
/** Called when the job reached a final state. */
|
|
30
|
+
onDone?: (job: EnrichJob) => void;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function EnrichmentJobDetail({ jobId, onDone, className }: EnrichmentJobDetailProps) {
|
|
35
|
+
const job = useJobPoll<EnrichJob>(
|
|
36
|
+
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, undefined, { signal })).data,
|
|
37
|
+
{ intervalMs: 2000, isTerminal: (j) => isJobTerminal(j.status), onDone },
|
|
38
|
+
);
|
|
39
|
+
const cancel = useG8Mutation(() => g8Post(`/enrichment/jobs/${jobId}/cancel`, {}));
|
|
40
|
+
const j = job.status;
|
|
41
|
+
const problems = job.done && j != null && ((j.successful_enrichments ?? 0) === 0 || (j.failed_enrichments ?? 0) > 0);
|
|
42
|
+
const errors = useG8Query(
|
|
43
|
+
async (signal) => (await g8Get<EnrichJob>(`/enrichment/jobs/${jobId}`, { include_provider_errors: true }, { signal })).data.provider_errors ?? [],
|
|
44
|
+
[jobId, job.done],
|
|
45
|
+
{ enabled: problems },
|
|
46
|
+
);
|
|
47
|
+
const total = j?.total ?? 0;
|
|
48
|
+
const pct = total ? Math.min(100, Math.round(((j?.completed ?? 0) / total) * 100)) : 0;
|
|
49
|
+
|
|
50
|
+
if (job.error && !j) return <ApiError error={job.error} title="Could not read the job" onRetry={() => job.start()} />;
|
|
51
|
+
return (
|
|
52
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`} role="status">
|
|
53
|
+
<div className="flex items-center gap-2">
|
|
54
|
+
{j ? <StatusPill tone={toneForStatus(j.status)} dot>{humanizeStatus(j.status)}</StatusPill> : <span className="text-sm text-muted-foreground">Loading...</span>}
|
|
55
|
+
{!job.done && j ? <Button size="sm" variant="outline" loading={cancel.loading} onClick={() => void cancel.mutate(undefined)}>Cancel job</Button> : null}
|
|
56
|
+
</div>
|
|
57
|
+
<div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary" style={{ width: `${job.done ? 100 : pct}%` }} /></div>
|
|
58
|
+
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel" /> : null}
|
|
59
|
+
<SectionCard title="Result">
|
|
60
|
+
<KeyValueList columns={2} items={[
|
|
61
|
+
{ label: "Job id", value: jobId, mono: true, wide: true },
|
|
62
|
+
{ label: "Processed", value: j ? `${(j.completed ?? 0).toLocaleString()} of ${total.toLocaleString()}` : "" },
|
|
63
|
+
{ label: "Enriched", value: j?.successful_enrichments != null ? j.successful_enrichments.toLocaleString() : "" },
|
|
64
|
+
{ label: "Failed", value: j?.failed_enrichments != null ? j.failed_enrichments.toLocaleString() : "" },
|
|
65
|
+
{ label: "Credits used", value: j?.total_credits_used != null ? j.total_credits_used.toLocaleString() : "" },
|
|
66
|
+
]} />
|
|
67
|
+
</SectionCard>
|
|
68
|
+
{j?.warnings?.length ? <SectionCard title="Warnings"><ul className="list-disc pl-5 text-sm">{j.warnings.map((w) => <li key={w}>{w}</li>)}</ul></SectionCard> : null}
|
|
69
|
+
{errors.data?.length ? (
|
|
70
|
+
<SectionCard title="Provider errors" description="Usually a missing or invalid provider key.">
|
|
71
|
+
<ul className="list-disc pl-5 text-sm text-destructive" aria-label="Provider errors">
|
|
72
|
+
{errors.data.slice(0, 20).map((e, i) => <li key={i}>{e.provider}: {e.error}</li>)}
|
|
73
|
+
</ul>
|
|
74
|
+
</SectionCard>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnrichmentScreen: the finished enrichment page, in four tabs (`?tab=`).
|
|
5
|
+
* - Enrich a list: pick a list, tick contacts, run a confirmed bulk enrichment (preflight, price, progress, cancel);
|
|
6
|
+
* open a contact to see its record and look it up or verify its email.
|
|
7
|
+
* - Look up: find one person or company by email, LinkedIn, name and domain (2 credits only when found) and verify
|
|
8
|
+
* an email address (1 credit). Nothing is written to a record without a tick.
|
|
9
|
+
* - Pipelines: the saved provider waterfalls of a list, create and delete them.
|
|
10
|
+
* - Providers: which providers graph8 funds, which need your own key, and what each action costs.
|
|
11
|
+
*
|
|
12
|
+
* Composes: ScreenLayout, StatStrip, SplitView, DetailPanel, SectionCard, KeyValueList, StatusPill (screen kit),
|
|
13
|
+
* BulkEnrichPanel, WaterfallConfig, EnrichButton, EmailVerifyBadge, ContactTable / ContactDetail (crm), ListPicker (lists).
|
|
14
|
+
* Provider keys are added in graph8 settings (no API), so the Providers tab links out for them.
|
|
15
|
+
*
|
|
16
|
+
* SDK operations (inside the parts it composes): listEnrichmentProviders, preflightEnrichment (read),
|
|
17
|
+
* startEnrichment (billable, ConfirmSpendDialog), getEnrichmentJob, cancelEnrichmentJob, lookupPerson / lookupCompany
|
|
18
|
+
* (billable, confirmed), verifyEmail (billable, confirmed), listWaterfallConfigs, createDefaultWaterfallConfig (billable,
|
|
19
|
+
* confirmed), deleteWaterfallConfig (destructive, confirmed), listContacts, getContact.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useEffect, useState } from "react";
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { Building2, ExternalLink, KeyRound, Sparkles, UserSearch } from "lucide-react";
|
|
25
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
26
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
27
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
28
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
29
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
30
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
31
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
32
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
33
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
34
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
35
|
+
import { ContactDetail } from "@/g8/crm/components/contact-detail";
|
|
36
|
+
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
37
|
+
import type { ContactRecord } from "@/g8/crm/lib/crm-types";
|
|
38
|
+
import { ListPicker } from "@/g8/lists/components/list-picker";
|
|
39
|
+
import { plural } from "@/g8/lib/plural";
|
|
40
|
+
import { useProviders } from "../hooks/use-providers";
|
|
41
|
+
import { CREDITS_PER_EMAIL_VERIFICATION, CREDITS_PER_FOUND_LOOKUP } from "../lib/enrichment-types";
|
|
42
|
+
import { BulkEnrichPanel } from "./bulk-enrich-panel";
|
|
43
|
+
import { EmailVerifyBadge } from "./email-verify-badge";
|
|
44
|
+
import { EnrichButton } from "./enrich-button";
|
|
45
|
+
import { EnrichmentJobDetail } from "./enrichment-job-detail";
|
|
46
|
+
import { WaterfallConfig } from "./waterfall-config";
|
|
47
|
+
import type { WaterfallConfigSummary } from "../lib/enrichment-types";
|
|
48
|
+
import { useBrand, brandUrl } from "@/g8/hooks/use-brand";
|
|
49
|
+
|
|
50
|
+
export interface EnrichmentScreenProps {
|
|
51
|
+
toolbarActions?: ReactNode;
|
|
52
|
+
/** Extra buttons in the opened contact's header. */
|
|
53
|
+
renderDetailActions?: (contact: ContactRecord) => ReactNode;
|
|
54
|
+
extraDetailTabs?: { id: string; label: string; render: (contact: ContactRecord) => ReactNode }[];
|
|
55
|
+
/** Where provider keys are managed. Default: the graph8 app. */
|
|
56
|
+
providerSettingsUrl?: string;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TABS = [{ id: "list", label: "Enrich a list" }, { id: "lookup", label: "Look up" }, { id: "pipelines", label: "Pipelines" }, { id: "jobs", label: "Jobs" }, { id: "providers", label: "Providers" }];
|
|
61
|
+
const FIELD = "flex flex-col gap-1";
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const JOBS_KEY = "g8.enrichment.jobs";
|
|
65
|
+
function loadJobs(): string[] {
|
|
66
|
+
try { const v = JSON.parse(window.sessionStorage.getItem(JOBS_KEY) ?? "[]"); return Array.isArray(v) ? v.filter((x) => typeof x === "string") : []; } catch { return []; }
|
|
67
|
+
}
|
|
68
|
+
function rememberJobs(ids: string[]): void {
|
|
69
|
+
try { window.sessionStorage.setItem(JOBS_KEY, JSON.stringify(ids.slice(0, 30))); } catch { /* private mode: the list lives for this page only */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function PipelineDetail({ config, onClose, onExpand, onCollapse }: { config: WaterfallConfigSummary; onClose: () => void; onExpand?: () => void; onCollapse?: () => void }) {
|
|
73
|
+
const brand = useBrand();
|
|
74
|
+
return (
|
|
75
|
+
<DetailPanel onExpand={onExpand} onCollapse={onCollapse} title={config.name} subtitle={`Fills ${config.field_to_enrich ?? "a field"} in ${plural(config.step_count, "step")}`} onClose={onClose}
|
|
76
|
+
badges={config.is_active === false ? <StatusPill tone="neutral" dot>Inactive</StatusPill> : <StatusPill tone="positive" dot>Active</StatusPill>}>
|
|
77
|
+
<div className="flex flex-col gap-4 p-1">
|
|
78
|
+
{config.missing_credentials.length ? <p role="alert" className="text-sm text-amber-700">No key for {config.missing_credentials.join(", ")}: those steps are skipped when it runs. Add the keys in {brand.linkLabel} settings.</p> : null}
|
|
79
|
+
<ol className="flex flex-col gap-2" aria-label="Pipeline steps">
|
|
80
|
+
{(config.providers_detail.length ? config.providers_detail : config.providers.map((p) => ({ provider: p, is_system_provider: false, credential_source: "system" as const, credential_ready: true }))).map((p, i) => (
|
|
81
|
+
<li key={`${p.provider}-${i}`} className="flex items-center gap-2 text-sm">
|
|
82
|
+
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p.provider}</span>
|
|
83
|
+
<StatusPill tone={p.credential_ready ? "positive" : "warning"} dot>{p.credential_source === "byok" ? "Your key" : `${brand.linkLabel} credits`}{p.credential_ready ? "" : ", not ready"}</StatusPill>
|
|
84
|
+
</li>
|
|
85
|
+
))}
|
|
86
|
+
</ol>
|
|
87
|
+
<KeyValueList columns={2} items={[
|
|
88
|
+
{ label: "Scope", value: config.is_global ? "All lists" : config.list_id != null ? `List #${config.list_id}` : "" },
|
|
89
|
+
{ label: "Pipeline id", value: config.column_id, mono: true, wide: true },
|
|
90
|
+
]} />
|
|
91
|
+
<p className="text-xs text-muted-foreground">Delete it from the list on the left. Values it already wrote stay on the contacts.</p>
|
|
92
|
+
</div>
|
|
93
|
+
</DetailPanel>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function JobsTab({ jobs, jobParam, setJobParam }: { jobs: string[]; jobParam: string; setJobParam: (v: string | null) => void }) {
|
|
98
|
+
const [typed, setTyped] = useState("");
|
|
99
|
+
const [view, setView] = useUrlState("view");
|
|
100
|
+
const full = view === "full";
|
|
101
|
+
const list = (
|
|
102
|
+
<div className="flex flex-col gap-3 p-1">
|
|
103
|
+
<SectionCard title="Open a job" description="Paste a job id from the API or an earlier run.">
|
|
104
|
+
<form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); if (typed.trim()) setJobParam(typed.trim()); }}>
|
|
105
|
+
<Input aria-label="Job id" value={typed} onChange={(e) => setTyped(e.target.value)} placeholder="Job id" />
|
|
106
|
+
<Button type="submit" size="sm" variant="outline" disabled={!typed.trim()}>Open</Button>
|
|
107
|
+
</form>
|
|
108
|
+
</SectionCard>
|
|
109
|
+
<SectionCard title="Started in this session" flush>
|
|
110
|
+
{jobs.length === 0 ? <EmptyState title="No jobs yet" description="Enrich contacts and the job appears here, with live progress." /> : (
|
|
111
|
+
<ul className="divide-y">{jobs.map((id) => (
|
|
112
|
+
<li key={id}><button type="button" className={`w-full px-4 py-2 text-left text-sm font-mono ${id === jobParam ? "bg-muted/50" : ""}`} onClick={() => setJobParam(id)}>{id}</button></li>
|
|
113
|
+
))}</ul>
|
|
114
|
+
)}
|
|
115
|
+
</SectionCard>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
return (
|
|
119
|
+
<SplitView expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)} list={list} detailOpen={Boolean(jobParam)} onCloseDetail={() => setJobParam(null)} detailTitle="Enrichment job"
|
|
120
|
+
detail={jobParam ? <DetailPanel key={jobParam} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} title="Enrichment job" subtitle={jobParam} onClose={() => setJobParam(null)}><EnrichmentJobDetail jobId={jobParam} /></DetailPanel> : null} />
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ListRail({ listId, onPick }: { listId: number | null; onPick: (id: number) => void }) {
|
|
125
|
+
return (
|
|
126
|
+
<SectionCard title="List" description="The contacts must be on a list.">
|
|
127
|
+
<ListPicker value={listId} allowCreate={false} onChange={(l) => onPick(l.id)} />
|
|
128
|
+
</SectionCard>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function LookupTab() {
|
|
133
|
+
const [email, setEmail] = useState("");
|
|
134
|
+
const [linkedin, setLinkedin] = useState("");
|
|
135
|
+
const [first, setFirst] = useState("");
|
|
136
|
+
const [last, setLast] = useState("");
|
|
137
|
+
const [domain, setDomain] = useState("");
|
|
138
|
+
const [cDomain, setCDomain] = useState("");
|
|
139
|
+
const [cName, setCName] = useState("");
|
|
140
|
+
const [verifyEmail, setVerifyEmail] = useState("");
|
|
141
|
+
const [contactRecord, setContactRecord] = useState("");
|
|
142
|
+
const [companyRecord, setCompanyRecord] = useState("");
|
|
143
|
+
const [applied, setApplied] = useState<string | null>(null);
|
|
144
|
+
const person = { email: email.trim() || undefined, linkedin_url: linkedin.trim() || undefined, first_name: first.trim() || undefined, last_name: last.trim() || undefined, company_domain: domain.trim() || undefined };
|
|
145
|
+
const company = { domain: cDomain.trim() || undefined, name: cName.trim() || undefined };
|
|
146
|
+
return (
|
|
147
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
148
|
+
<SectionCard title="Find a person" description={`${CREDITS_PER_FOUND_LOOKUP} credits, only when a match is found.`} actions={<UserSearch className="size-4 text-muted-foreground" aria-hidden />}>
|
|
149
|
+
<div className="flex flex-col gap-3">
|
|
150
|
+
<div className={FIELD}><Label htmlFor="lk-email">Work email</Label><Input id="lk-email" value={email} onChange={(e) => setEmail(e.target.value)} /></div>
|
|
151
|
+
<div className={FIELD}><Label htmlFor="lk-li">LinkedIn URL</Label><Input id="lk-li" value={linkedin} onChange={(e) => setLinkedin(e.target.value)} /></div>
|
|
152
|
+
<div className="grid grid-cols-2 gap-2">
|
|
153
|
+
<div className={FIELD}><Label htmlFor="lk-first">First name</Label><Input id="lk-first" value={first} onChange={(e) => setFirst(e.target.value)} /></div>
|
|
154
|
+
<div className={FIELD}><Label htmlFor="lk-last">Last name</Label><Input id="lk-last" value={last} onChange={(e) => setLast(e.target.value)} /></div>
|
|
155
|
+
</div>
|
|
156
|
+
<div className={FIELD}><Label htmlFor="lk-domain">Company domain</Label><Input id="lk-domain" placeholder="acme.com" value={domain} onChange={(e) => setDomain(e.target.value)} /></div>
|
|
157
|
+
<p className="text-xs text-muted-foreground">Use an email, a LinkedIn URL, or first name, last name and company domain together.</p>
|
|
158
|
+
<div className={FIELD}><Label htmlFor="lk-rec">Apply to contact id (optional)</Label><Input id="lk-rec" inputMode="numeric" className="w-40" value={contactRecord} onChange={(e) => setContactRecord(e.target.value)} /></div>
|
|
159
|
+
<div className="flex items-center gap-2">
|
|
160
|
+
<EnrichButton kind="person" lookup={person} record={Number(contactRecord) > 0 ? { id: Number(contactRecord) } : undefined} label="Find person" variant="default" size="default" onApplied={() => setApplied(`Updated contact ${contactRecord}.`)} />
|
|
161
|
+
{applied ? <span role="status" className="text-sm">{applied}</span> : null}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</SectionCard>
|
|
165
|
+
<div className="flex flex-col gap-4">
|
|
166
|
+
<SectionCard title="Find a company" description={`${CREDITS_PER_FOUND_LOOKUP} credits, only when a match is found.`} actions={<Building2 className="size-4 text-muted-foreground" aria-hidden />}>
|
|
167
|
+
<div className="flex flex-col gap-3">
|
|
168
|
+
<div className={FIELD}><Label htmlFor="lk-cdomain">Domain</Label><Input id="lk-cdomain" placeholder="acme.com" value={cDomain} onChange={(e) => setCDomain(e.target.value)} /></div>
|
|
169
|
+
<div className={FIELD}><Label htmlFor="lk-cname">Name</Label><Input id="lk-cname" value={cName} onChange={(e) => setCName(e.target.value)} /></div>
|
|
170
|
+
<div className={FIELD}><Label htmlFor="lk-crec">Apply to company id (optional)</Label><Input id="lk-crec" inputMode="numeric" className="w-40" value={companyRecord} onChange={(e) => setCompanyRecord(e.target.value)} /></div>
|
|
171
|
+
<div><EnrichButton kind="company" lookup={company} record={Number(companyRecord) > 0 ? { id: Number(companyRecord) } : undefined} label="Find company" variant="default" size="default" /></div>
|
|
172
|
+
</div>
|
|
173
|
+
</SectionCard>
|
|
174
|
+
<SectionCard title="Verify an email" description={`${CREDITS_PER_EMAIL_VERIFICATION} credit per check.`}>
|
|
175
|
+
<div className="flex flex-col gap-3">
|
|
176
|
+
<div className={FIELD}><Label htmlFor="vf-email">Email address</Label><Input id="vf-email" value={verifyEmail} onChange={(e) => setVerifyEmail(e.target.value)} /></div>
|
|
177
|
+
<EmailVerifyBadge key={verifyEmail.trim()} email={verifyEmail.trim()} />
|
|
178
|
+
</div>
|
|
179
|
+
</SectionCard>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function ProvidersTab({ settingsUrl }: { settingsUrl: string }) {
|
|
186
|
+
const brand = useBrand();
|
|
187
|
+
const catalog = useProviders();
|
|
188
|
+
if (catalog.error) return <ApiError error={catalog.error} title="Could not load providers" onRetry={() => void catalog.refetch()} />;
|
|
189
|
+
if (!catalog.loading && catalog.providers.length === 0) return <EmptyState title="No providers" description="The provider catalog is empty for this workspace." />;
|
|
190
|
+
return (
|
|
191
|
+
<SectionCard title="Providers" description={`Providers on ${brand.linkLabel}'s key spend credits when they run. Providers on your own key cost no ${brand.linkLabel} credits.`}
|
|
192
|
+
actions={<a className="inline-flex items-center gap-1 text-sm underline" href={settingsUrl} target="_blank" rel="noreferrer"><KeyRound className="size-4" aria-hidden />Manage keys in {brand.linkLabel}<ExternalLink className="size-3" aria-hidden /></a>}>
|
|
193
|
+
<ul className="divide-y">
|
|
194
|
+
{catalog.providers.map((p) => (
|
|
195
|
+
<li key={p.provider} className="flex flex-wrap items-center gap-3 py-2" data-testid="provider-row">
|
|
196
|
+
<span className="w-36 text-sm font-medium">{p.provider}</span>
|
|
197
|
+
{p.byok_configured ? <StatusPill tone="positive" dot>Your key set</StatusPill> : p.system_funded ? <StatusPill tone="info" dot>{brand.linkLabel} funded</StatusPill> : <StatusPill tone="warning" dot>Needs your key</StatusPill>}
|
|
198
|
+
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">{p.actions.join(", ")}</span>
|
|
199
|
+
<span className="text-xs tabular-nums text-muted-foreground">{Object.entries(p.credits_per_row).map(([k, v]) => `${k} ${v}`).join(" / ")}</span>
|
|
200
|
+
</li>
|
|
201
|
+
))}
|
|
202
|
+
</ul>
|
|
203
|
+
</SectionCard>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function EnrichmentScreen({ toolbarActions, renderDetailActions, extraDetailTabs, providerSettingsUrl: providerSettingsUrlProp, className }: EnrichmentScreenProps) {
|
|
208
|
+
const brand = useBrand();
|
|
209
|
+
const providerSettingsUrl = providerSettingsUrlProp ?? brandUrl(brand, "/settings");
|
|
210
|
+
const [tab, setTab] = useUrlState("tab", "list");
|
|
211
|
+
const [listParam, setListParam] = useUrlState("list");
|
|
212
|
+
const [picked, setPicked] = useState<number[]>([]);
|
|
213
|
+
const [opened, setOpened] = useState<ContactRecord | null>(null);
|
|
214
|
+
const catalog = useProviders(tab === "providers");
|
|
215
|
+
const [jobParam, setJobParam] = useUrlState("job");
|
|
216
|
+
const [pipelineParam, setPipelineParam] = useUrlState("pipeline");
|
|
217
|
+
const [view, setView] = useUrlState("view");
|
|
218
|
+
const full = view === "full";
|
|
219
|
+
const [jobs, setJobs] = useState<string[]>([]);
|
|
220
|
+
useEffect(() => setJobs(loadJobs()), []);
|
|
221
|
+
const started = (id: string) => setJobs((cur) => { const next = [id, ...cur.filter((x) => x !== id)]; rememberJobs(next); return next; });
|
|
222
|
+
const listId = listParam ? Number(listParam) : null;
|
|
223
|
+
const pick = (id: number) => { setListParam(String(id)); setPicked([]); setOpened(null); setPipelineParam(null); };
|
|
224
|
+
|
|
225
|
+
const stats = tab === "providers" && catalog.providers.length > 0 ? [
|
|
226
|
+
{ label: "Providers", value: catalog.providers.length },
|
|
227
|
+
{ label: `${brand.linkLabel} funded`, value: catalog.providers.filter((p) => p.system_funded).length },
|
|
228
|
+
{ label: "Your keys set", value: catalog.providers.filter((p) => p.byok_configured).length },
|
|
229
|
+
] : undefined;
|
|
230
|
+
|
|
231
|
+
let body: ReactNode;
|
|
232
|
+
// WaterfallConfig owns the pipelines request and hands the result up, so the detail view needs no second fetch.
|
|
233
|
+
const [loaded, setLoaded] = useState<{ listId: number | null; configs: WaterfallConfigSummary[] }>({ listId: null, configs: [] });
|
|
234
|
+
const openPipeline = (loaded.listId === listId ? loaded.configs : []).find((c) => c.column_id === pipelineParam);
|
|
235
|
+
if (tab === "lookup") body = <LookupTab />;
|
|
236
|
+
else if (tab === "jobs") body = <JobsTab jobs={jobs} jobParam={jobParam} setJobParam={setJobParam} />;
|
|
237
|
+
else if (tab === "providers") body = <ProvidersTab settingsUrl={providerSettingsUrl} />;
|
|
238
|
+
else {
|
|
239
|
+
body = (
|
|
240
|
+
<div className="grid gap-4 lg:grid-cols-[18rem_minmax(0,1fr)]">
|
|
241
|
+
<ListRail listId={listId} onPick={pick} />
|
|
242
|
+
{listId == null ? (
|
|
243
|
+
<EmptyState icon={<Sparkles className="size-6" aria-hidden />} title="Choose a list" description={tab === "pipelines" ? "Pipelines belong to a list. Pick one to see and create them." : "Pick a list, tick the contacts to fill in, and enrich them."} />
|
|
244
|
+
) : tab === "pipelines" ? (
|
|
245
|
+
<SplitView
|
|
246
|
+
list={<WaterfallConfig key={listId} listId={listId} selectedId={pipelineParam || null} onOpen={(c) => setPipelineParam(c.column_id)} onLoaded={(configs) => setLoaded({ listId, configs })} />}
|
|
247
|
+
expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)} detailOpen={openPipeline != null} onCloseDetail={() => setPipelineParam(null)} detailTitle="Pipeline"
|
|
248
|
+
detail={openPipeline ? <PipelineDetail config={openPipeline} onClose={() => setPipelineParam(null)} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} /> : null} />
|
|
249
|
+
) : (
|
|
250
|
+
<div className="grid min-w-0 gap-4 xl:grid-cols-2">
|
|
251
|
+
<ContactTable key={listId} listId={listId} selectable pageSize={10} onSelectionChange={(ids) => setPicked(ids)} onOpen={setOpened} />
|
|
252
|
+
<div className="flex min-w-0 flex-col gap-4">
|
|
253
|
+
<SectionCard title={`Enrich ${picked.length} selected`} description="Fill work email or phones with a provider waterfall. You confirm the price first.">
|
|
254
|
+
{picked.length === 0 ? <p className="text-sm text-muted-foreground">Tick contacts on the left.</p> : <BulkEnrichPanel key={listId} listId={listId} contactIds={picked} onStarted={started} />}
|
|
255
|
+
</SectionCard>
|
|
256
|
+
{opened ? (
|
|
257
|
+
<DetailPanel key={opened.id} title={opened.full_name ?? ([opened.first_name, opened.last_name].filter(Boolean).join(" ") || `Contact #${opened.id}`)} subtitle={opened.job_title} onClose={() => setOpened(null)}>
|
|
258
|
+
<ContactDetail contactId={opened.id} extraTabs={extraDetailTabs}
|
|
259
|
+
renderActions={(c) => (<>
|
|
260
|
+
<EnrichButton kind="person" record={c as ContactRecord & Record<string, unknown>} lookup={{ email: c.work_email ?? undefined, linkedin_url: c.linkedin_url ?? undefined, first_name: c.first_name ?? undefined, last_name: c.last_name ?? undefined, company_domain: c.company_domain ?? undefined }} />
|
|
261
|
+
<EmailVerifyBadge email={c.work_email} />
|
|
262
|
+
{renderDetailActions?.(c)}
|
|
263
|
+
</>)} />
|
|
264
|
+
</DetailPanel>
|
|
265
|
+
) : null}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<ScreenLayout
|
|
275
|
+
className={className}
|
|
276
|
+
title="Enrichment"
|
|
277
|
+
description="Fill in what you do not know about your contacts: emails, phones and company facts. Every paid step shows its price first."
|
|
278
|
+
icon={<Sparkles className="size-5" aria-hidden />}
|
|
279
|
+
actions={toolbarActions}
|
|
280
|
+
tabs={TABS}
|
|
281
|
+
activeTab={TABS.some((t) => t.id === tab) ? tab : "list"}
|
|
282
|
+
onTabChange={setTab}
|
|
283
|
+
stats={stats}
|
|
284
|
+
>
|
|
285
|
+
{body}
|
|
286
|
+
</ScreenLayout>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
* 1. listWaterfallConfigs GET /enrichment/waterfall/configs?list_id (read) pipelines runnable on the list
|
|
9
9
|
* 2. listEnrichmentProviders GET /enrichment/providers (read) graph8-funded vs own key, credits per action
|
|
10
10
|
* 3. createDefaultWaterfallConfig POST /enrichment/waterfall/configs { list_id, name, field, providers, skip_existing_values }
|
|
11
|
-
* (billable tier) goes through ConfirmSpendDialog first
|
|
11
|
+
* (billable tier) goes through ConfirmSpendDialog first. With `field` (always sent here) it is idempotent per field and
|
|
12
|
+
* list: when the list already has a pipeline for that field the API returns it with `created: false` and its own
|
|
13
|
+
* providers, whatever name and providers were sent. The form then says so instead of claiming a new pipeline.
|
|
12
14
|
* 4. deleteWaterfallConfig DELETE /enrichment/waterfall/configs/{column_id} (destructive, ConfirmSpendDialog)
|
|
13
15
|
*
|
|
14
16
|
* Providers on graph8's system key spend credits when the pipeline RUNS; providers on the org's own key (BYOK) do not.
|
|
15
17
|
* A BYOK provider without a key is listed under `missing_credentials` and is skipped at run time.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
import { useState } from "react";
|
|
20
|
+
import { useEffect, useRef, useState } from "react";
|
|
19
21
|
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
20
22
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -23,36 +25,57 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
23
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
26
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
27
|
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import { plural } from "@/g8/lib/plural";
|
|
26
29
|
import { useProviders } from "../hooks/use-providers";
|
|
27
30
|
import { TARGET_FIELDS } from "../lib/enrichment-types";
|
|
28
31
|
import type { TargetField, WaterfallConfigSummary } from "../lib/enrichment-types";
|
|
32
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
29
33
|
|
|
30
34
|
export interface WaterfallConfigProps {
|
|
31
35
|
listId: number;
|
|
32
36
|
/** Called with the pipeline's column_id after create or delete (for example to refresh a BulkEnrichPanel). */
|
|
33
37
|
onChanged?: () => void;
|
|
38
|
+
/** Makes each pipeline's name open a detail view in your layout. Omit it and the list is unchanged. */
|
|
39
|
+
onOpen?: (config: WaterfallConfigSummary) => void;
|
|
40
|
+
/** Highlights the open pipeline (its column_id). */
|
|
41
|
+
selectedId?: string | null;
|
|
42
|
+
/** Called with the loaded pipelines each time they load or reload, so a screen can show one in a detail view without a second request. */
|
|
43
|
+
onLoaded?: (configs: WaterfallConfigSummary[]) => void;
|
|
34
44
|
className?: string;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfigProps) {
|
|
47
|
+
export function WaterfallConfig({ listId, onChanged, onOpen, selectedId, onLoaded, className }: WaterfallConfigProps) {
|
|
48
|
+
const brand = useBrand();
|
|
38
49
|
const [name, setName] = useState("");
|
|
39
50
|
const [field, setField] = useState<TargetField>("work_email");
|
|
40
51
|
const [order, setOrder] = useState<string[]>([]);
|
|
41
52
|
const [skipExisting, setSkipExisting] = useState(true);
|
|
42
53
|
const [confirmCreate, setConfirmCreate] = useState(false);
|
|
43
54
|
const [deleting, setDeleting] = useState<WaterfallConfigSummary | null>(null);
|
|
55
|
+
// The answer to the last create when the API reused the list's pipeline for that field instead of creating one.
|
|
56
|
+
const [reused, setReused] = useState<{ name?: string; field: TargetField; providers: string[]; asked: string[] } | null>(null);
|
|
44
57
|
const catalog = useProviders();
|
|
45
58
|
|
|
46
59
|
const configs = useG8Query((signal) => g8Get<{ configs: WaterfallConfigSummary[] }>("/enrichment/waterfall/configs", { list_id: listId }, { signal }), [listId]);
|
|
47
60
|
const create = useG8Mutation(
|
|
48
|
-
() => g8Post<{ column_id: string; created: boolean }>("/enrichment/waterfall/configs", { list_id: listId, name: name.trim(), field, providers: order, skip_existing_values: skipExisting }),
|
|
49
|
-
{
|
|
61
|
+
() => g8Post<{ column_id: string; created: boolean; name?: string; providers?: string[] }>("/enrichment/waterfall/configs", { list_id: listId, name: name.trim(), field, providers: order, skip_existing_values: skipExisting }),
|
|
62
|
+
{
|
|
63
|
+
onSuccess: (res) => {
|
|
64
|
+
setReused(res.data.created === false ? { name: res.data.name, field, providers: res.data.providers ?? [], asked: order } : null);
|
|
65
|
+
setConfirmCreate(false); setName(""); setOrder([]); void configs.refetch(); onChanged?.();
|
|
66
|
+
},
|
|
67
|
+
// A failed create must not leave the previous answer's "Already exists, reused" next to the error.
|
|
68
|
+
onError: () => setReused(null),
|
|
69
|
+
},
|
|
50
70
|
);
|
|
51
71
|
const remove = useG8Mutation((columnId: string) => g8Delete(`/enrichment/waterfall/configs/${columnId}`), {
|
|
52
72
|
onSuccess: () => { setDeleting(null); void configs.refetch(); onChanged?.(); },
|
|
53
73
|
});
|
|
54
74
|
|
|
55
75
|
const items = configs.data?.data.configs ?? [];
|
|
76
|
+
const loadedRef = useRef(onLoaded);
|
|
77
|
+
loadedRef.current = onLoaded;
|
|
78
|
+
useEffect(() => { if (configs.data) loadedRef.current?.(configs.data.data.configs ?? []); }, [configs.data]);
|
|
56
79
|
const byName = new Map(catalog.providers.map((p) => [p.provider, p]));
|
|
57
80
|
const systemUsed = order.filter((p) => byName.get(p)?.system_funded && !byName.get(p)?.byok_configured);
|
|
58
81
|
const move = (p: string, d: number) => {
|
|
@@ -73,12 +96,12 @@ export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfi
|
|
|
73
96
|
{!configs.loading && !configs.error && items.length === 0 ? <EmptyState title="No pipelines yet" description="Create one below, or let the first enrichment run create a default." /> : null}
|
|
74
97
|
<ul className="flex flex-col gap-2">
|
|
75
98
|
{items.map((c) => (
|
|
76
|
-
<li key={c.column_id} className=
|
|
99
|
+
<li key={c.column_id} className={`rounded-md border p-3 ${selectedId === c.column_id ? "bg-muted/50" : ""}`} data-testid="pipeline">
|
|
77
100
|
<div className="flex items-center justify-between gap-2">
|
|
78
|
-
<p className="text-sm font-medium">{c.name}{c.is_global ? <span className="ml-2 text-xs text-muted-foreground">all lists</span> : null}</p>
|
|
101
|
+
<p className="text-sm font-medium">{onOpen ? <button type="button" className="underline-offset-2 hover:underline" onClick={() => onOpen(c)}>{c.name}</button> : c.name}{c.is_global ? <span className="ml-2 text-xs text-muted-foreground">all lists</span> : null}</p>
|
|
79
102
|
<Button variant="ghost" size="sm" onClick={() => setDeleting(c)}>Delete</Button>
|
|
80
103
|
</div>
|
|
81
|
-
<p className="text-xs text-muted-foreground">{c.field_to_enrich ?? "field"}: {c.providers.join(" > ") || "no providers"} ({c.step_count
|
|
104
|
+
<p className="text-xs text-muted-foreground">{c.field_to_enrich ?? "field"}: {c.providers.join(" > ") || "no providers"} ({plural(c.step_count, "step")})</p>
|
|
82
105
|
{c.missing_credentials.length ? <p role="alert" className="mt-1 text-xs text-amber-700">Skipped at run time, no key: {c.missing_credentials.join(", ")}</p> : null}
|
|
83
106
|
</li>
|
|
84
107
|
))}
|
|
@@ -100,7 +123,7 @@ export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfi
|
|
|
100
123
|
{order.map((p, i) => (
|
|
101
124
|
<li key={p} className="flex items-center gap-2 text-sm">
|
|
102
125
|
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="w-32 font-medium">{p}</span>
|
|
103
|
-
<span className="text-xs text-muted-foreground">{byName.get(p)?.byok_configured ? "your key, free" : byName.get(p)?.system_funded ?
|
|
126
|
+
<span className="text-xs text-muted-foreground">{byName.get(p)?.byok_configured ? "your key, free" : byName.get(p)?.system_funded ? `${brand.linkLabel} credits` : "needs your key"}</span>
|
|
104
127
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} up`} disabled={i === 0} onClick={() => move(p, -1)}>Up</Button>
|
|
105
128
|
<Button variant="ghost" size="sm" aria-label={`Move ${p} down`} disabled={i === order.length - 1} onClick={() => move(p, 1)}>Down</Button>
|
|
106
129
|
<Button variant="ghost" size="sm" aria-label={`Remove ${p}`} onClick={() => setOrder(order.filter((x) => x !== p))}>Remove</Button>
|
|
@@ -114,11 +137,17 @@ export function WaterfallConfig({ listId, onChanged, className }: WaterfallConfi
|
|
|
114
137
|
</div>
|
|
115
138
|
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={skipExisting} onChange={(e) => setSkipExisting(e.target.checked)} /> Skip contacts that already have a value</label>
|
|
116
139
|
{create.error ? <ApiError error={create.error} title="Could not create the pipeline" /> : null}
|
|
140
|
+
{reused ? (
|
|
141
|
+
<p role="status" className="text-sm text-muted-foreground">
|
|
142
|
+
Already exists, reused: this list already has a {TARGET_FIELDS.find((f) => f.value === reused.field)?.label.toLowerCase() ?? reused.field} pipeline{reused.name ? ` ("${reused.name}")` : ""}, so no new one was created.
|
|
143
|
+
{reused.providers.length && reused.providers.join() !== reused.asked.join() ? ` It keeps its providers: ${reused.providers.join(" > ")}. Delete it first to use ${reused.asked.join(" > ")}.` : ""}
|
|
144
|
+
</p>
|
|
145
|
+
) : null}
|
|
117
146
|
<div className="flex justify-end"><Button disabled={!name.trim() || order.length === 0} onClick={() => setConfirmCreate(true)}>Create pipeline</Button></div>
|
|
118
147
|
</section>
|
|
119
148
|
|
|
120
149
|
<ConfirmSpendDialog open={confirmCreate} tier="billable" title={`Create pipeline "${name.trim()}"`}
|
|
121
|
-
description={systemUsed.length ? `Running it uses
|
|
150
|
+
description={systemUsed.length ? `Running it uses ${brand.linkLabel} credits for ${systemUsed.join(", ")}; providers on your own key cost nothing. Creating the pipeline itself does not run it.` : `All providers use your own keys, so running it costs no ${brand.linkLabel} credits. Creating the pipeline does not run it.`}
|
|
122
151
|
confirmLabel="Create" confirming={create.loading} onConfirm={() => void create.mutate(undefined)} onCancel={() => setConfirmCreate(false)} />
|
|
123
152
|
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete pipeline "${deleting?.name ?? ""}"`} description="Values it already wrote stay on the contacts."
|
|
124
153
|
affected={1} confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.column_id)} onCancel={() => setDeleting(null)} />
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @title Create a waterfall pipeline for a list and see which providers are ready
|
|
3
3
|
* @when Setting up "try provider A, then B" enrichment for a column once, then reusing it. Check the provider
|
|
4
4
|
* catalog first: graph8-funded providers spend credits when the pipeline runs, providers on your own key do not,
|
|
5
|
-
* and a provider without a key is skipped. Creating is idempotent by (list_id, name).
|
|
5
|
+
* and a provider without a key is skipped. Creating with `field` is idempotent per field and list: if the list already has a pipeline for that field it is returned with `created: false` and keeps its own providers, whatever name or providers you send (delete it first to change them). Without `field`, a pipeline is reused by (list_id, name).
|
|
6
6
|
*/
|
|
7
7
|
import { g8 } from "@graph8/sdk";
|
|
8
8
|
|
|
@@ -17,7 +17,8 @@ export async function createPipeline(listId: number) {
|
|
|
17
17
|
const res = await g8.api.enrichment.createDefaultWaterfallConfig({
|
|
18
18
|
body: { list_id: listId, name: "Work email waterfall", field: "work_email", providers: ["prospeo", "dropcontact"], skip_existing_values: true },
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
// created === false: the list already had a work-email pipeline; it was returned as is (check res.data.providers).
|
|
21
|
+
return { columnId: res.data.column_id, created: res.data.created, providers: res.data.providers, missing: res.data.missing_credentials };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function pipelinesOf(listId: number) {
|
package/blocks/forms/README.md
CHANGED
|
@@ -5,6 +5,17 @@ snippet, lists them, and routes every submission to a list with a field mapping.
|
|
|
5
5
|
that: install the code (`FormEmbed`), set where leads go and how fields map (`FormBuilder`), see what arrived and
|
|
6
6
|
retry failures (`SubmissionsTable`), and find forms that are not captured (`SiteAuditPanel`).
|
|
7
7
|
|
|
8
|
+
## Screens
|
|
9
|
+
|
|
10
|
+
`FormsScreen` (`components/forms-screen.tsx`) is the finished page: install code, captured forms, routing and submissions in one place.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { FormsScreen } from "@/g8/forms/components/forms-screen";
|
|
14
|
+
export default function Page() { return <FormsScreen />; }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Flows: copy the snippet and ready-made lead form (Install tab, `?tab=install`), see every captured form with health and counts (search is debounced; the first 100 are listed), open one (deep link `?id=<source_id>/<form_key>`; a submission opens in a sheet at `&submission=<id>` with every field, its delivery and trace) to rename it, pause or resume delivery (confirmed), choose the destination list and map fields (suggest, preview, save, test), read submissions, trace a lead and retry a failed delivery (confirmed), and find forms that are not captured (Site audit tab, `?tab=audit`). A `&submission=` link opened cold (reload, shared link) is looked up in the form's latest 100 submissions; an older one shows its trace and points to the Submissions tab. Optional props: `renderRowActions`, `renderDetailActions`, `extraDetailTabs`, `toolbarActions`.
|
|
18
|
+
|
|
8
19
|
## Install
|
|
9
20
|
|
|
10
21
|
```
|
|
@@ -78,6 +89,7 @@ export default function FormsPage() {
|
|
|
78
89
|
|
|
79
90
|
| Component | File | What it is |
|
|
80
91
|
|---|---|---|
|
|
92
|
+
| `FormsScreen` | `components/forms-screen.tsx` | The finished page: install code, captured forms, routing, submissions and site audit. |
|
|
81
93
|
| `FormBuilder` | `components/form-builder.tsx` | Forms workspace: list captured forms, rename, pause/resume delivery, set destination and mapping, view submissions. |
|
|
82
94
|
| `FormMappingEditor` | `components/form-mapping-editor.tsx` | Pick the destination list and map submitted fields to contact fields, with suggest, preview, save and test. |
|
|
83
95
|
| `FormEmbed` | `components/form-embed.tsx` | Copyable tracking snippet (script tag, React) and a ready-made lead-capture form with live preview. |
|
package/blocks/forms/block.json
CHANGED
|
@@ -15,28 +15,48 @@
|
|
|
15
15
|
"crm"
|
|
16
16
|
],
|
|
17
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
18
|
"get_contact_form_fields_contacts_form_fields_get",
|
|
25
|
-
"
|
|
19
|
+
"get_form_fields_forms__form_key__fields_get",
|
|
20
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
26
21
|
"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
22
|
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
34
|
-
"
|
|
23
|
+
"get_form_template_snippet_form_get",
|
|
24
|
+
"get_org_snippet_snippet_get",
|
|
35
25
|
"get_site_audit_forms_site_audit_get",
|
|
26
|
+
"list_forms_forms_get",
|
|
27
|
+
"list_lists_lists_get",
|
|
28
|
+
"pause_form_delivery_forms__form_key__delivery_pause_post",
|
|
29
|
+
"preview_form_mapping_forms__form_key__mapping_preview_post",
|
|
36
30
|
"request_site_audit_scan_forms_site_audit_scan_post",
|
|
31
|
+
"resume_form_delivery_forms__form_key__delivery_resume_post",
|
|
32
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
33
|
+
"save_form_mapping_forms__form_key__mapping_save_post",
|
|
34
|
+
"search_form_submissions_forms__form_key__submissions_post",
|
|
35
|
+
"test_form_route_forms__form_key__route_test_post",
|
|
36
|
+
"update_form_registry_forms__form_key__patch",
|
|
37
37
|
"update_site_audit_finding_forms_site_audit_findings__finding_id__patch"
|
|
38
38
|
],
|
|
39
39
|
"components": [
|
|
40
|
+
{
|
|
41
|
+
"name": "FormsScreen",
|
|
42
|
+
"file": "components/forms-screen.tsx",
|
|
43
|
+
"purpose": "Finished forms workspace: captured forms with health, detail (overview, destination and fields, submissions), install code and site audit tabs.",
|
|
44
|
+
"operations": [
|
|
45
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
46
|
+
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
47
|
+
"list_forms_forms_get",
|
|
48
|
+
"pause_form_delivery_forms__form_key__delivery_pause_post",
|
|
49
|
+
"resume_form_delivery_forms__form_key__delivery_resume_post",
|
|
50
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
51
|
+
"search_form_submissions_forms__form_key__submissions_post",
|
|
52
|
+
"update_form_registry_forms__form_key__patch"
|
|
53
|
+
],
|
|
54
|
+
"tiers": [
|
|
55
|
+
"read",
|
|
56
|
+
"write",
|
|
57
|
+
"external"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
40
60
|
{
|
|
41
61
|
"name": "FormBuilder",
|
|
42
62
|
"file": "components/form-builder.tsx",
|
|
@@ -90,9 +110,10 @@
|
|
|
90
110
|
"file": "components/submissions-table.tsx",
|
|
91
111
|
"purpose": "Paged submissions with delivery state, a per-lead trace and a confirmed retry for failed deliveries.",
|
|
92
112
|
"operations": [
|
|
93
|
-
"search_form_submissions_forms__form_key__submissions_post",
|
|
94
113
|
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
95
|
-
"
|
|
114
|
+
"list_forms_forms_get",
|
|
115
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
116
|
+
"search_form_submissions_forms__form_key__submissions_post"
|
|
96
117
|
],
|
|
97
118
|
"tiers": [
|
|
98
119
|
"read",
|
|
@@ -195,6 +216,7 @@
|
|
|
195
216
|
}
|
|
196
217
|
],
|
|
197
218
|
"peerDependencies": {
|
|
198
|
-
"react": ">=18"
|
|
219
|
+
"react": ">=18",
|
|
220
|
+
"lucide-react": ">=0.400.0"
|
|
199
221
|
}
|
|
200
222
|
}
|