@graph8/sdk 0.362.0 → 0.373.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 +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -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-load-more-list.ts +61 -0
- 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-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -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 +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- 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/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- 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 +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- 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 +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- 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 +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- 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 +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- 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 +19 -0
- package/blocks/social-ads/block.json +233 -1
- 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 +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- 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 +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- 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 +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- 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 +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- 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 +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -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 +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -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/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
package/blocks/forms/README.md
CHANGED
|
@@ -122,3 +122,11 @@ Tiers: `read` and `write` need no confirmation; `external` (pause, resume, retry
|
|
|
122
122
|
| `forms.getSiteAudit` | `GET /api/v1/forms/site-audit` | read | `forms:read` | SiteAuditPanel |
|
|
123
123
|
| `forms.requestSiteAuditScan` | `POST /api/v1/forms/site-audit/scan` | write | `forms:write` | SiteAuditPanel |
|
|
124
124
|
| `forms.updateSiteAuditFinding` | `PATCH /api/v1/forms/site-audit/findings/{finding_id}` | write | `forms:write` | SiteAuditPanel |
|
|
125
|
+
|
|
126
|
+
## Depth added (C7c)
|
|
127
|
+
|
|
128
|
+
graph8 captures forms from your site; there is **no create-form operation in the API** (an API gap, not a missing screen).
|
|
129
|
+
|
|
130
|
+
New on `FormsScreen`: a **Groups** tab (`FormGroups`: create, rename, re-type, add and remove forms, delete), saved views (`FormSavedViews`), "Load more" past the first 100 forms, and on an open form: a **Routing** tab (`FormBackfill`: preview, then a confirmed delivery; `FormRouteHistory`: restore the previous route or stop routing, both destructive confirms that send the version they read), an **Activity** tab (`FormActivity`: audit trail and pages), and **Export CSV** on Submissions (`SubmissionsExport`, formula-safe, says when the file is not the whole set).
|
|
131
|
+
|
|
132
|
+
Not covered (and why): `saveFormRoute` (the mapping editor's save already writes the route); `captureFormPreview` and `getFormPreview` return untyped image data; unidentified-form grouping (`previewUnidentifiedGrouping`, `setUnidentifiedGrouping`, `syncUnidentifiedGroup`) needs a list picker (owned by the pickers PR). The mapping editor's list select keeps its existing cap until that PR lands.
|
package/blocks/forms/block.json
CHANGED
|
@@ -15,25 +15,38 @@
|
|
|
15
15
|
"crm"
|
|
16
16
|
],
|
|
17
17
|
"operations": [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"resume_form_delivery_forms__form_key__delivery_resume_post",
|
|
23
|
-
"get_form_fields_forms__form_key__fields_get",
|
|
18
|
+
"add_form_group_members_forms_groups__group_id__members_post",
|
|
19
|
+
"create_form_backfill_forms__form_key__backfills_post",
|
|
20
|
+
"create_form_group_forms_groups_post",
|
|
21
|
+
"delete_form_group_forms_groups__group_id__delete",
|
|
24
22
|
"get_contact_form_fields_contacts_form_fields_get",
|
|
25
|
-
"
|
|
23
|
+
"get_form_activity_forms__form_key__activity_get",
|
|
24
|
+
"get_form_backfill_forms__form_key__backfills__job_id__get",
|
|
25
|
+
"get_form_fields_forms__form_key__fields_get",
|
|
26
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
27
|
+
"get_form_placements_forms__form_key__placements_get",
|
|
26
28
|
"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
29
|
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
34
|
-
"
|
|
30
|
+
"get_form_template_snippet_form_get",
|
|
31
|
+
"get_org_snippet_snippet_get",
|
|
35
32
|
"get_site_audit_forms_site_audit_get",
|
|
33
|
+
"list_form_groups_forms_groups_get",
|
|
34
|
+
"list_forms_forms_get",
|
|
35
|
+
"list_lists_lists_get",
|
|
36
|
+
"list_saved_form_views_forms_views_saved_get",
|
|
37
|
+
"pause_form_delivery_forms__form_key__delivery_pause_post",
|
|
38
|
+
"preview_form_mapping_forms__form_key__mapping_preview_post",
|
|
39
|
+
"remove_form_group_members_forms_groups__group_id__members_remove_post",
|
|
36
40
|
"request_site_audit_scan_forms_site_audit_scan_post",
|
|
41
|
+
"resume_form_delivery_forms__form_key__delivery_resume_post",
|
|
42
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
43
|
+
"rollback_form_route_forms__form_key__route_delete",
|
|
44
|
+
"save_form_mapping_forms__form_key__mapping_save_post",
|
|
45
|
+
"save_form_view_forms_views_saved__view_id__put",
|
|
46
|
+
"search_form_submissions_forms__form_key__submissions_post",
|
|
47
|
+
"test_form_route_forms__form_key__route_test_post",
|
|
48
|
+
"update_form_group_forms_groups__group_id__patch",
|
|
49
|
+
"update_form_registry_forms__form_key__patch",
|
|
37
50
|
"update_site_audit_finding_forms_site_audit_findings__finding_id__patch"
|
|
38
51
|
],
|
|
39
52
|
"components": [
|
|
@@ -41,8 +54,21 @@
|
|
|
41
54
|
"name": "FormsScreen",
|
|
42
55
|
"file": "components/forms-screen.tsx",
|
|
43
56
|
"purpose": "Finished forms workspace: captured forms with health, detail (overview, destination and fields, submissions), install code and site audit tabs.",
|
|
44
|
-
"operations": [
|
|
45
|
-
|
|
57
|
+
"operations": [
|
|
58
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
59
|
+
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
60
|
+
"list_forms_forms_get",
|
|
61
|
+
"pause_form_delivery_forms__form_key__delivery_pause_post",
|
|
62
|
+
"resume_form_delivery_forms__form_key__delivery_resume_post",
|
|
63
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
64
|
+
"search_form_submissions_forms__form_key__submissions_post",
|
|
65
|
+
"update_form_registry_forms__form_key__patch"
|
|
66
|
+
],
|
|
67
|
+
"tiers": [
|
|
68
|
+
"read",
|
|
69
|
+
"write",
|
|
70
|
+
"external"
|
|
71
|
+
]
|
|
46
72
|
},
|
|
47
73
|
{
|
|
48
74
|
"name": "FormBuilder",
|
|
@@ -97,9 +123,10 @@
|
|
|
97
123
|
"file": "components/submissions-table.tsx",
|
|
98
124
|
"purpose": "Paged submissions with delivery state, a per-lead trace and a confirmed retry for failed deliveries.",
|
|
99
125
|
"operations": [
|
|
100
|
-
"search_form_submissions_forms__form_key__submissions_post",
|
|
101
126
|
"get_form_submission_trace_forms__form_key__submissions__submission_id__trace_get",
|
|
102
|
-
"
|
|
127
|
+
"list_forms_forms_get",
|
|
128
|
+
"retry_form_delivery_forms_deliveries__receipt_id__retry_post",
|
|
129
|
+
"search_form_submissions_forms__form_key__submissions_post"
|
|
103
130
|
],
|
|
104
131
|
"tiers": [
|
|
105
132
|
"read",
|
|
@@ -119,6 +146,95 @@
|
|
|
119
146
|
"read",
|
|
120
147
|
"write"
|
|
121
148
|
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"name": "FormActivity",
|
|
152
|
+
"file": "components/form-activity.tsx",
|
|
153
|
+
"purpose": "Audit trail of one form and the pages it appears on.",
|
|
154
|
+
"operations": [
|
|
155
|
+
"get_form_activity_forms__form_key__activity_get",
|
|
156
|
+
"get_form_placements_forms__form_key__placements_get"
|
|
157
|
+
],
|
|
158
|
+
"tiers": [
|
|
159
|
+
"read"
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "FormBackfill",
|
|
164
|
+
"file": "components/form-backfill.tsx",
|
|
165
|
+
"purpose": "Deliver earlier captured leads through the form route: preview, then a confirmed delivery.",
|
|
166
|
+
"operations": [
|
|
167
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
168
|
+
"get_form_backfill_forms__form_key__backfills__job_id__get",
|
|
169
|
+
"create_form_backfill_forms__form_key__backfills_post"
|
|
170
|
+
],
|
|
171
|
+
"tiers": [
|
|
172
|
+
"read",
|
|
173
|
+
"external"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "FormGroups",
|
|
178
|
+
"file": "components/form-groups.tsx",
|
|
179
|
+
"purpose": "Curated groups of forms: create, rename, add and remove forms, delete.",
|
|
180
|
+
"operations": [
|
|
181
|
+
"list_forms_forms_get",
|
|
182
|
+
"update_form_group_forms_groups__group_id__patch",
|
|
183
|
+
"add_form_group_members_forms_groups__group_id__members_post",
|
|
184
|
+
"remove_form_group_members_forms_groups__group_id__members_remove_post",
|
|
185
|
+
"delete_form_group_forms_groups__group_id__delete",
|
|
186
|
+
"list_form_groups_forms_groups_get",
|
|
187
|
+
"create_form_group_forms_groups_post"
|
|
188
|
+
],
|
|
189
|
+
"tiers": [
|
|
190
|
+
"read",
|
|
191
|
+
"write",
|
|
192
|
+
"destructive"
|
|
193
|
+
]
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "FormRouteHistory",
|
|
197
|
+
"file": "components/form-route-history.tsx",
|
|
198
|
+
"purpose": "Undo a routing change: restore the previous route or stop routing the form.",
|
|
199
|
+
"operations": [
|
|
200
|
+
"get_form_overview_forms__form_key__overview_get",
|
|
201
|
+
"rollback_form_route_forms__form_key__route_delete"
|
|
202
|
+
],
|
|
203
|
+
"tiers": [
|
|
204
|
+
"read",
|
|
205
|
+
"destructive"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"name": "FormSavedViews",
|
|
210
|
+
"file": "components/form-saved-views.tsx",
|
|
211
|
+
"purpose": "Save and reapply the forms list search and health filter.",
|
|
212
|
+
"operations": [
|
|
213
|
+
"list_saved_form_views_forms_views_saved_get",
|
|
214
|
+
"save_form_view_forms_views_saved__view_id__put"
|
|
215
|
+
],
|
|
216
|
+
"tiers": [
|
|
217
|
+
"read",
|
|
218
|
+
"write"
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "MoreRows",
|
|
223
|
+
"file": "components/more-rows.tsx",
|
|
224
|
+
"purpose": "Footer for paged lists: how many rows are shown and a Load more button.",
|
|
225
|
+
"operations": [],
|
|
226
|
+
"tiers": []
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"name": "SubmissionsExport",
|
|
230
|
+
"file": "components/submissions-export.tsx",
|
|
231
|
+
"purpose": "Download a form submissions as a CSV file.",
|
|
232
|
+
"operations": [
|
|
233
|
+
"search_form_submissions_forms__form_key__submissions_post"
|
|
234
|
+
],
|
|
235
|
+
"tiers": [
|
|
236
|
+
"read"
|
|
237
|
+
]
|
|
122
238
|
}
|
|
123
239
|
],
|
|
124
240
|
"hooks": [],
|
|
@@ -126,6 +242,10 @@
|
|
|
126
242
|
{
|
|
127
243
|
"file": "lib/form-types.ts",
|
|
128
244
|
"purpose": "Form/submission types and tolerant readers for passthrough responses, status labels, mapping payload builder."
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
"file": "lib/forms-extra.ts",
|
|
248
|
+
"purpose": "Helpers for the forms management components: open-dict readers, paging adapter, date and CSV helpers."
|
|
129
249
|
}
|
|
130
250
|
],
|
|
131
251
|
"recipes": [
|
|
@@ -173,6 +293,7 @@
|
|
|
173
293
|
],
|
|
174
294
|
"scopes": [
|
|
175
295
|
"contacts:read",
|
|
296
|
+
"forms:delete",
|
|
176
297
|
"forms:read",
|
|
177
298
|
"forms:run",
|
|
178
299
|
"forms:write",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormActivity: the audit trail of one form (route saved, delivery paused, backfill queued...) and the pages it was
|
|
5
|
+
* seen on.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. getFormActivity GET /forms/{form_key}/activity?source_id (read; newest first)
|
|
9
|
+
* 2. getFormPlacements GET /forms/{form_key}/placements?source_id (read; the page URLs the form was seen on)
|
|
10
|
+
*
|
|
11
|
+
* Both payloads are open dicts: events show whatever of `action`, `actor_id`, `created_at` and `details` is present.
|
|
12
|
+
* The API returns a bounded history (the latest events); when it comes back full the screen says so.
|
|
13
|
+
*/
|
|
14
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
17
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import type { FormRef } from "../lib/form-types";
|
|
21
|
+
import { rowsOf, str, when } from "../lib/forms-extra";
|
|
22
|
+
import type { Rec } from "../lib/forms-extra";
|
|
23
|
+
|
|
24
|
+
export interface FormActivityProps {
|
|
25
|
+
form: FormRef;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** How many events the activity read is expected to hold at most; a full read gets a notice. */
|
|
30
|
+
export const ACTIVITY_WINDOW = 500;
|
|
31
|
+
|
|
32
|
+
const words = (v: unknown): string => str(v).replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
33
|
+
|
|
34
|
+
export function FormActivity({ form, className }: FormActivityProps) {
|
|
35
|
+
const base = `/forms/${encodeURIComponent(form.form_key)}`;
|
|
36
|
+
const q = { source_id: form.source_id };
|
|
37
|
+
const activity = useG8Query((signal) => g8Get(`${base}/activity`, q, { signal }), [form.form_key, form.source_id]);
|
|
38
|
+
const placements = useG8Query((signal) => g8Get(`${base}/placements`, q, { signal }), [form.form_key, form.source_id]);
|
|
39
|
+
const events = rowsOf<Rec>(activity.data?.data, "events", "activity");
|
|
40
|
+
const pages = rowsOf<Rec>(placements.data?.data, "placements");
|
|
41
|
+
return (
|
|
42
|
+
<div className={className}>
|
|
43
|
+
<div className="grid gap-4">
|
|
44
|
+
<SectionCard title="Where this form appears" description="Pages the form was seen on.">
|
|
45
|
+
{placements.error ? <ApiError error={placements.error} title="Could not load the pages" onRetry={() => void placements.refetch()} /> : placements.loading && !placements.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : pages.length === 0 ? (
|
|
46
|
+
<EmptyState compact title="No pages recorded yet" description="A page is recorded when the form is submitted from it." />
|
|
47
|
+
) : (
|
|
48
|
+
<ul className="grid gap-1">{pages.map((p, i) => <li key={i} className="truncate text-sm">{str(p.page_url) || str(p.url) || str(p.path)}{p.last_seen_at ? <span className="text-muted-foreground text-xs"> · last seen {when(p.last_seen_at)}</span> : null}</li>)}</ul>
|
|
49
|
+
)}
|
|
50
|
+
</SectionCard>
|
|
51
|
+
<SectionCard title="Activity" description="Changes made to how this form is routed.">
|
|
52
|
+
{activity.error ? <ApiError error={activity.error} title="Could not load the activity" onRetry={() => void activity.refetch()} /> : activity.loading && !activity.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : events.length === 0 ? (
|
|
53
|
+
<EmptyState compact title="No activity yet" description="Changes to the destination, mapping and delivery are listed here." />
|
|
54
|
+
) : (
|
|
55
|
+
<>
|
|
56
|
+
<ol className="grid gap-2">
|
|
57
|
+
{events.map((e, i) => (
|
|
58
|
+
<li key={str(e.id) || i} className="rounded-md border p-2 text-sm">
|
|
59
|
+
<span className="font-medium">{words(e.action) || words(e.type) || "Change"}</span>
|
|
60
|
+
<span className="text-muted-foreground text-xs"> {when(e.created_at)}{str(e.actor_id) ? ` · by ${str(e.actor_id)}` : ""}</span>
|
|
61
|
+
</li>
|
|
62
|
+
))}
|
|
63
|
+
</ol>
|
|
64
|
+
{events.length >= ACTIVITY_WINDOW ? <p role="note" className="text-muted-foreground mt-2 text-xs">Showing the latest {ACTIVITY_WINDOW} events. Older ones are not listed.</p> : null}
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</SectionCard>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormBackfill: deliver leads that were captured BEFORE the form had a destination (or while it was paused) through the
|
|
5
|
+
* form's current route. Preview first, then deliver. Delivering creates or updates contacts and cannot be undone.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. getFormOverview GET /forms/{form_key}/overview?source_id (read; the destination, the route version and how many leads were captured)
|
|
9
|
+
* 2. createFormBackfill POST /forms/{form_key}/backfills { source_id, start_at?, end_at?, preview: true } (records a PREVIEW job; delivers nothing)
|
|
10
|
+
* 3. createFormBackfill POST /forms/{form_key}/backfills { ..., preview: false, estimated } (EXTERNAL, confirmed: delivers leads)
|
|
11
|
+
* 4. getFormBackfill GET /forms/{form_key}/backfills/{job} (read; polled every 3 s while the job is queued or running)
|
|
12
|
+
*
|
|
13
|
+
* A form with no destination cannot be backfilled: the screen says so and points at "Destination and fields". The API
|
|
14
|
+
* does not count the leads in a window, so the confirm states the most that can be delivered (all captured leads) and
|
|
15
|
+
* the dates only narrow it. Dates are whole days in UTC: the end day is included.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useState } from "react";
|
|
18
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
22
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
23
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
24
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import type { FormOverview, FormRef } from "../lib/form-types";
|
|
29
|
+
import { asRec, dayInstant, num, str } from "../lib/forms-extra";
|
|
30
|
+
|
|
31
|
+
export interface FormBackfillProps {
|
|
32
|
+
form: FormRef;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ACTIVE = new Set(["queued", "running"]);
|
|
37
|
+
|
|
38
|
+
export function FormBackfill({ form, className }: FormBackfillProps) {
|
|
39
|
+
const base = `/forms/${encodeURIComponent(form.form_key)}`;
|
|
40
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
41
|
+
const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, { source_id: form.source_id }, { signal }).then((r) => r.data), [form.form_key, form.source_id]);
|
|
42
|
+
const [from, setFrom] = useState("");
|
|
43
|
+
const [to, setTo] = useState("");
|
|
44
|
+
const [jobId, setJobId] = useState("");
|
|
45
|
+
const job = useG8Query((signal) => g8Get(`${base}/backfills/${encodeURIComponent(jobId)}`, undefined, { signal }), [jobId], { enabled: jobId !== "" });
|
|
46
|
+
const j = asRec(job.data?.data);
|
|
47
|
+
const state = str(j.state);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (jobId === "" || !ACTIVE.has(state)) return;
|
|
51
|
+
const t = setTimeout(() => void job.refetch(), 3000);
|
|
52
|
+
return () => clearTimeout(t);
|
|
53
|
+
}, [jobId, state, job.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
54
|
+
|
|
55
|
+
const startAt = dayInstant(from);
|
|
56
|
+
const endAt = dayInstant(to, true);
|
|
57
|
+
const reversed = startAt !== undefined && endAt !== undefined && startAt >= endAt;
|
|
58
|
+
// A legacy-mapping form has a destination but no route: the API refuses a backfill for it (409), so gate on the route.
|
|
59
|
+
const hasDestination = Boolean(overview.data?.route);
|
|
60
|
+
const captured = overview.data?.metrics?.captured ?? 0;
|
|
61
|
+
const window = { ...(startAt ? { start_at: startAt } : {}), ...(endAt ? { end_at: endAt } : {}) };
|
|
62
|
+
const preview = useG8Mutation(async () => { const res = await g8Post(`${base}/backfills`, { source_id: form.source_id, ...window, preview: true }); setJobId(str(asRec(res.data).id)); });
|
|
63
|
+
|
|
64
|
+
const deliver = (): void => ask({
|
|
65
|
+
tier: "external",
|
|
66
|
+
title: `Deliver captured leads from "${form.form_key}"`,
|
|
67
|
+
confirmLabel: "Deliver leads",
|
|
68
|
+
recipients: captured,
|
|
69
|
+
description: `Runs the saved leads through the current destination${overview.data?.destination?.list_name ? ` (${overview.data.destination.list_name})` : ""}, creating or updating contacts. Up to ${captured.toLocaleString()} captured leads, fewer if you set dates. This cannot be undone.`,
|
|
70
|
+
run: async () => { const res = await g8Post(`${base}/backfills`, { source_id: form.source_id, ...window, preview: false, estimated: captured }); setJobId(str(asRec(res.data).id)); },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SectionCard className={className} title="Backfill earlier leads" description="Send leads captured before the destination was set up.">
|
|
75
|
+
{overview.error ? <ApiError error={overview.error} title="Could not load the form" onRetry={() => void overview.refetch()} /> : !overview.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : !hasDestination ? (
|
|
76
|
+
<p className="text-sm">This form has no destination route yet. Set one in Destination and fields, then come back.</p>
|
|
77
|
+
) : (
|
|
78
|
+
<div className="grid gap-3">
|
|
79
|
+
<p className="text-sm">{captured.toLocaleString()} captured {captured === 1 ? "lead" : "leads"} are available to deliver to <strong>{overview.data.destination?.list_name ?? "the destination"}</strong>.</p>
|
|
80
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
81
|
+
<div><Label htmlFor="fb-from" className="mb-1">From (optional)</Label><Input id="fb-from" type="date" value={from} onChange={(e) => setFrom(e.target.value)} /></div>
|
|
82
|
+
<div><Label htmlFor="fb-to" className="mb-1">Through (optional)</Label><Input id="fb-to" type="date" value={to} onChange={(e) => setTo(e.target.value)} aria-invalid={reversed} /></div>
|
|
83
|
+
<Button variant="outline" loading={preview.loading} disabled={reversed} onClick={() => void preview.mutate(undefined)}>Preview</Button>
|
|
84
|
+
<Button disabled={reversed || captured === 0 || ACTIVE.has(state)} onClick={deliver}>Deliver leads</Button>
|
|
85
|
+
</div>
|
|
86
|
+
{reversed ? <p role="alert" className="text-error-foreground text-xs">The start day must be before the end day.</p> : null}
|
|
87
|
+
{captured === 0 ? <p className="text-muted-foreground text-xs">Nothing has been captured yet.</p> : null}
|
|
88
|
+
{jobId !== "" ? (
|
|
89
|
+
job.error ? <ApiError error={job.error} title="Could not read the backfill" onRetry={() => void job.refetch()} /> : (
|
|
90
|
+
<div className="grid gap-2" role="status">
|
|
91
|
+
<p className="flex items-center gap-2 text-sm">Backfill <StatusPill tone={state === "completed" ? "positive" : state === "failed" ? "negative" : state === "preview" ? "neutral" : "info"}>{state || "starting"}</StatusPill></p>
|
|
92
|
+
<KeyValueList columns={2} items={[
|
|
93
|
+
{ label: "Estimated leads", value: num(j.estimated) == null ? "" : String(j.estimated) },
|
|
94
|
+
{ label: "Delivered", value: num(j.delivered) == null ? (num(j.processed) == null ? "" : String(j.processed)) : String(j.delivered) },
|
|
95
|
+
{ label: "Failed", value: num(j.failed) == null ? "" : String(j.failed) },
|
|
96
|
+
]} />
|
|
97
|
+
{state === "preview" ? <p className="text-muted-foreground text-xs">This was a preview. Nothing was delivered.</p> : null}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{preview.error ? <ApiError error={preview.error} title="Could not preview" /> : null}
|
|
104
|
+
{failure ? <ApiError error={failure} title="Could not start the backfill" /> : null}
|
|
105
|
+
<ConfirmSpendDialog {...dialog} />
|
|
106
|
+
</SectionCard>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -28,6 +28,7 @@ import { STATUS_LABEL, formLabel, formStatus, pageUrlText, pickItems } from "../
|
|
|
28
28
|
import type { FormListItem, FormOverview, FormRef } from "../lib/form-types";
|
|
29
29
|
import { FormMappingEditor } from "./form-mapping-editor";
|
|
30
30
|
import { SubmissionsTable } from "./submissions-table";
|
|
31
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
32
|
|
|
32
33
|
export interface FormBuilderProps {
|
|
33
34
|
/** Form to open first. */
|
|
@@ -67,7 +68,7 @@ function FormHeader({ form, onChanged }: { form: FormListItem; onChanged: () =>
|
|
|
67
68
|
))}
|
|
68
69
|
</dl>
|
|
69
70
|
) : null}
|
|
70
|
-
{overview.data?.delivery_warning ? <p className="text-sm text-
|
|
71
|
+
{overview.data?.delivery_warning ? <p className="text-sm text-warning-foreground">{overview.data.delivery_warning}</p> : null}
|
|
71
72
|
<ConfirmSpendDialog
|
|
72
73
|
open={confirm !== null}
|
|
73
74
|
tier="external"
|
|
@@ -83,6 +84,7 @@ function FormHeader({ form, onChanged }: { form: FormListItem; onChanged: () =>
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export function FormBuilder({ initialForm, onSelectForm }: FormBuilderProps) {
|
|
87
|
+
const brand = useBrand();
|
|
86
88
|
const [page, setPage] = useState(1);
|
|
87
89
|
const [searchText, setSearchText] = useState("");
|
|
88
90
|
const [search, setSearch] = useState("");
|
|
@@ -109,7 +111,7 @@ export function FormBuilder({ initialForm, onSelectForm }: FormBuilderProps) {
|
|
|
109
111
|
<Button type="submit" size="sm" variant="outline">Search</Button>
|
|
110
112
|
</form>
|
|
111
113
|
{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." :
|
|
114
|
+
<EmptyState title={search ? "No forms match" : "No forms captured yet"} description={search ? "Try another name." : `Install the ${brand.linkLabel} snippet on your site (see FormEmbed). Forms appear here after their first submission.`} />
|
|
113
115
|
) : (
|
|
114
116
|
<ul className="grid gap-1">
|
|
115
117
|
{items.map((f) => {
|
|
@@ -15,6 +15,7 @@ import { Button, Card, CardContent, CardHeader, CardTitle, Label, Skeleton } fro
|
|
|
15
15
|
import { ApiError } from "@/g8/components/api-error";
|
|
16
16
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
17
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
18
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
18
19
|
|
|
19
20
|
export interface FormEmbedProps {
|
|
20
21
|
/** Stream (site) to read the snippet for. Omit for the organization's main stream. */
|
|
@@ -55,6 +56,7 @@ function CodeBlock({ label, code }: { label: string; code: string }) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export function FormEmbed({ streamId }: FormEmbedProps) {
|
|
59
|
+
const brand = useBrand();
|
|
58
60
|
const [stream, setStream] = useState<string | undefined>(streamId);
|
|
59
61
|
const snippet = useG8Query((signal) => g8Get<Snippet>("/snippet", { stream_id: stream }, { signal }).then((r) => r.data), [stream]);
|
|
60
62
|
const template = useG8Query((signal) => g8Get<{ html?: string }>("/snippet/form", { stream_id: stream }, { signal }).then((r) => r.data), [stream]);
|
|
@@ -73,14 +75,14 @@ export function FormEmbed({ streamId }: FormEmbedProps) {
|
|
|
73
75
|
<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
76
|
{s.available_streams.map((st) => <option key={st.id} value={st.id}>{st.name}{st.domains?.length ? ` (${st.domains.join(", ")})` : ""}</option>)}
|
|
75
77
|
</select>
|
|
76
|
-
{s.warning ? <p className="text-sm text-
|
|
78
|
+
{s.warning ? <p className="text-sm text-warning-foreground">{s.warning}</p> : null}
|
|
77
79
|
</CardContent>
|
|
78
80
|
</Card>
|
|
79
81
|
) : null}
|
|
80
82
|
<Card>
|
|
81
83
|
<CardHeader><CardTitle>1. Install the tracking snippet</CardTitle></CardHeader>
|
|
82
84
|
<CardContent className="grid gap-3">
|
|
83
|
-
<p className="text-muted-foreground text-sm">Put this on every page that has a form.
|
|
85
|
+
<p className="text-muted-foreground text-sm">Put this on every page that has a form. {brand.linkLabel} then detects the forms and records submissions. Allowed domains: {s?.domains?.join(", ") || "any"}.</p>
|
|
84
86
|
{s?.script_tag ? <CodeBlock label="Script tag" code={s.script_tag} /> : null}
|
|
85
87
|
{s?.react_snippet ? <CodeBlock label="React component" code={s.react_snippet} /> : null}
|
|
86
88
|
<p className="text-muted-foreground text-xs">Write key: <code>{s?.write_key}</code> (public, safe in page source).</p>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormGroups: curated groups of forms ("Demo requests", "Newsletter sign-ups") so related forms are read together.
|
|
5
|
+
* Create a group, rename or re-type it, add and remove its forms, delete it. A form belongs to one group: adding it to
|
|
6
|
+
* another group moves it.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in the order the screen uses them:
|
|
9
|
+
* 1. listFormGroups GET /forms/groups (read)
|
|
10
|
+
* 2. listForms GET /forms?page&per_page&search (read; the forms you can add, paged with "Load more")
|
|
11
|
+
* 3. createFormGroup POST /forms/groups { name, form_type, members } (write)
|
|
12
|
+
* 4. updateFormGroup PATCH /forms/groups/{id} { name?, form_type? } (write; only changed fields are sent)
|
|
13
|
+
* 5. addFormGroupMembers POST /forms/groups/{id}/members { members } (write)
|
|
14
|
+
* 6. removeFormGroupMembers POST /forms/groups/{id}/members/remove { members } (write)
|
|
15
|
+
* 7. deleteFormGroup DELETE /forms/groups/{id} (destructive, confirmed: the forms themselves stay)
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
28
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { formLabel, pickItems } from "../lib/form-types";
|
|
30
|
+
import type { FormListItem } from "../lib/form-types";
|
|
31
|
+
import { memberRef, pageOf, rowsOf, str } from "../lib/forms-extra";
|
|
32
|
+
import type { Rec } from "../lib/forms-extra";
|
|
33
|
+
import { MoreRows } from "./more-rows";
|
|
34
|
+
|
|
35
|
+
export interface FormGroupsProps {
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const TYPES = ["lead", "newsletter", "meeting", "contact", "download", "other"] as const;
|
|
40
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
41
|
+
const PAGE = 50;
|
|
42
|
+
|
|
43
|
+
const keyOf = (f: { form_key: string; source_id: string }): string => `${f.source_id}/${f.form_key}`;
|
|
44
|
+
|
|
45
|
+
/** The forms that can be added, searchable and paged. Forms already picked are marked, never hidden. */
|
|
46
|
+
function FormPicker({ picked, onToggle }: { picked: Set<string>; onToggle: (f: FormListItem) => void }) {
|
|
47
|
+
const [search, setSearch] = useState("");
|
|
48
|
+
const list = usePaginated<FormListItem>(async (cursor, signal) => {
|
|
49
|
+
const page = cursor ? Number(cursor) : 1;
|
|
50
|
+
const res = await g8Get("/forms", { page, per_page: PAGE, search: search.trim() || undefined }, { signal });
|
|
51
|
+
const d = res.data as { items?: FormListItem[]; total?: number } | undefined;
|
|
52
|
+
const got = pickItems<FormListItem>(d, "items");
|
|
53
|
+
return pageOf(got, got.length > 0 && page * PAGE < (d?.total ?? 0) ? page + 1 : null);
|
|
54
|
+
}, [search]);
|
|
55
|
+
return (
|
|
56
|
+
<div className="grid gap-2">
|
|
57
|
+
<Input aria-label="Search forms to add" placeholder="Search forms" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
58
|
+
{list.error ? <ApiError error={list.error} title="Could not load forms" onRetry={() => void list.reload()} /> : list.loading ? <Skeleton className="h-16 w-full" aria-busy="true" /> : list.items.length === 0 ? (
|
|
59
|
+
<EmptyState compact title={search ? "No form matches" : "No forms captured yet"} />
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
<ul className="grid max-h-64 gap-1 overflow-y-auto">
|
|
63
|
+
{list.items.map((f) => (
|
|
64
|
+
<li key={keyOf(f)}>
|
|
65
|
+
<label className="flex items-center gap-2 rounded-md border p-2 text-sm">
|
|
66
|
+
<input type="checkbox" checked={picked.has(keyOf(f))} onChange={() => onToggle(f)} aria-label={`Include ${formLabel(f)}`} />
|
|
67
|
+
<span className="truncate">{formLabel(f)}</span>
|
|
68
|
+
</label>
|
|
69
|
+
</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
<MoreRows shown={list.items.length} noun="forms" hasMore={list.hasMore} loading={list.loadingMore} onMore={() => void list.loadMore()} />
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function GroupCard({ group, onChanged }: { group: Rec; onChanged: () => void }) {
|
|
80
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
81
|
+
const id = str(group.id);
|
|
82
|
+
const members = rowsOf<Rec>(group.members).map(memberRef);
|
|
83
|
+
const [name, setName] = useState(str(group.name));
|
|
84
|
+
const [type, setType] = useState(str(group.form_type, "other"));
|
|
85
|
+
const [adding, setAdding] = useState(false);
|
|
86
|
+
const [picked, setPicked] = useState<Map<string, { form_key: string; source_id: string }>>(new Map());
|
|
87
|
+
const changes: Rec = {};
|
|
88
|
+
if (name.trim() !== str(group.name)) changes.name = name.trim();
|
|
89
|
+
if (type !== str(group.form_type, "other")) changes.form_type = type;
|
|
90
|
+
const save = useG8Mutation(() => g8Patch(`/forms/groups/${encodeURIComponent(id)}`, changes), { onSuccess: onChanged });
|
|
91
|
+
const add = useG8Mutation(() => g8Post(`/forms/groups/${encodeURIComponent(id)}/members`, { members: [...picked.values()] }), { onSuccess: () => { setAdding(false); setPicked(new Map()); onChanged(); } });
|
|
92
|
+
const remove = useG8Mutation((m: { form_key: string; source_id: string }) => g8Post(`/forms/groups/${encodeURIComponent(id)}/members/remove`, { members: [m] }), { onSuccess: onChanged });
|
|
93
|
+
const toggle = (f: FormListItem): void => setPicked((cur) => { const n = new Map(cur); if (n.has(keyOf(f))) n.delete(keyOf(f)); else n.set(keyOf(f), { form_key: f.form_key, source_id: f.source_id }); return n; });
|
|
94
|
+
return (
|
|
95
|
+
<li className="grid gap-3 rounded-md border p-3">
|
|
96
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
97
|
+
<Input aria-label={`Group name ${str(group.name)}`} className="max-w-xs" value={name} onChange={(e) => setName(e.target.value)} />
|
|
98
|
+
<select aria-label={`Type of ${str(group.name)}`} className={SELECT} value={type} onChange={(e) => setType(e.target.value)}>{TYPES.map((t) => <option key={t} value={t}>{t[0].toUpperCase() + t.slice(1)}</option>)}</select>
|
|
99
|
+
<Button size="sm" variant="outline" loading={save.loading} disabled={Object.keys(changes).length === 0 || name.trim() === ""} onClick={() => void save.mutate(undefined)}>Save group</Button>
|
|
100
|
+
<Button size="sm" variant="ghost" className="ml-auto" aria-label={`Delete group ${str(group.name)}`} onClick={() => ask({
|
|
101
|
+
tier: "destructive", title: `Delete the group "${str(group.name)}"`, confirmLabel: "Delete group", affected: members.length,
|
|
102
|
+
description: "Only the group is removed. Its forms keep capturing and routing leads.", tolerateStatus: [404],
|
|
103
|
+
run: () => g8Delete(`/forms/groups/${encodeURIComponent(id)}`), onDone: onChanged,
|
|
104
|
+
})}>Delete</Button>
|
|
105
|
+
</div>
|
|
106
|
+
{members.length === 0 ? <p className="text-muted-foreground text-xs">No forms in this group yet.</p> : (
|
|
107
|
+
<ul className="flex flex-wrap gap-1">{members.map((m) => (
|
|
108
|
+
<li key={keyOf(m)} className="flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs">{m.form_key}<button type="button" aria-label={`Remove ${m.form_key} from ${str(group.name)}`} className="text-muted-foreground hover:text-foreground" onClick={() => void remove.mutate(m)}>x</button></li>
|
|
109
|
+
))}</ul>
|
|
110
|
+
)}
|
|
111
|
+
<div><Button size="sm" variant="outline" onClick={() => setAdding(true)}>Add forms</Button></div>
|
|
112
|
+
{[save.error, remove.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
|
|
113
|
+
{failure ? <ApiError error={failure} title="Could not delete the group" /> : null}
|
|
114
|
+
<FormSheet open={adding} onOpenChange={setAdding} title={`Add forms to ${str(group.name)}`} description="A form in another group moves here." width="lg"
|
|
115
|
+
footer={<><Button variant="outline" onClick={() => setAdding(false)}>Cancel</Button><Button loading={add.loading} disabled={picked.size === 0} onClick={() => void add.mutate(undefined)}>Add {picked.size || ""} forms</Button></>}>
|
|
116
|
+
<FormPicker picked={new Set(picked.keys())} onToggle={toggle} />
|
|
117
|
+
{add.error ? <ApiError error={add.error} title="Could not add the forms" /> : null}
|
|
118
|
+
</FormSheet>
|
|
119
|
+
<ConfirmSpendDialog {...dialog} />
|
|
120
|
+
</li>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function FormGroups({ className }: FormGroupsProps) {
|
|
125
|
+
const groups = useG8Query((signal) => g8Get("/forms/groups", undefined, { signal }), []);
|
|
126
|
+
const rows = rowsOf<Rec>(groups.data?.data, "groups");
|
|
127
|
+
const [creating, setCreating] = useState(false);
|
|
128
|
+
const [name, setName] = useState("");
|
|
129
|
+
const [type, setType] = useState<(typeof TYPES)[number]>("lead");
|
|
130
|
+
const [picked, setPicked] = useState<Map<string, { form_key: string; source_id: string }>>(new Map());
|
|
131
|
+
const create = useG8Mutation(() => g8Post("/forms/groups", { name: name.trim(), form_type: type, members: [...picked.values()] }), {
|
|
132
|
+
onSuccess: () => { setCreating(false); setName(""); setPicked(new Map()); void groups.refetch(); },
|
|
133
|
+
});
|
|
134
|
+
const toggle = (f: FormListItem): void => setPicked((cur) => { const n = new Map(cur); if (n.has(keyOf(f))) n.delete(keyOf(f)); else n.set(keyOf(f), { form_key: f.form_key, source_id: f.source_id }); return n; });
|
|
135
|
+
return (
|
|
136
|
+
<div className={className}>
|
|
137
|
+
<SectionCard title="Form groups" description="Keep related forms together." actions={<Button size="sm" onClick={() => setCreating(true)}>New group</Button>}>
|
|
138
|
+
{groups.error ? <ApiError error={groups.error} title="Could not load groups" onRetry={() => void groups.refetch()} /> : groups.loading && !groups.data ? <Skeleton className="h-20 w-full" aria-busy="true" /> : rows.length === 0 ? (
|
|
139
|
+
<EmptyState compact title="No groups yet" description="Group forms such as every demo request form." action={<Button onClick={() => setCreating(true)}>New group</Button>} />
|
|
140
|
+
) : <ul className="grid gap-3">{rows.map((g) => <GroupCard key={str(g.id)} group={g} onChanged={() => void groups.refetch()} />)}</ul>}
|
|
141
|
+
</SectionCard>
|
|
142
|
+
<FormSheet open={creating} onOpenChange={setCreating} title="New form group" description="Pick the forms that belong together." width="lg"
|
|
143
|
+
footer={<><Button variant="outline" onClick={() => setCreating(false)}>Cancel</Button><Button loading={create.loading} disabled={name.trim() === ""} onClick={() => void create.mutate(undefined)}>Create group</Button></>}>
|
|
144
|
+
<div><Label htmlFor="fg-name" className="mb-1">Name</Label><Input id="fg-name" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
145
|
+
<div><Label htmlFor="fg-type" className="mb-1">Type</Label><select id="fg-type" className={`${SELECT} w-full`} value={type} onChange={(e) => setType(e.target.value as (typeof TYPES)[number])}>{TYPES.map((t) => <option key={t} value={t}>{t[0].toUpperCase() + t.slice(1)}</option>)}</select></div>
|
|
146
|
+
<FormPicker picked={new Set(picked.keys())} onToggle={toggle} />
|
|
147
|
+
{create.error ? <ApiError error={create.error} title="Could not create the group" /> : null}
|
|
148
|
+
</FormSheet>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|