@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SurveyResults: the headline score, response rate, answer split and trend for one survey.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getSurveySummary (GET /feedback/surveys/{survey_id}/summary?bucket) read score, response rate, split
|
|
8
|
+
* 2. getSurveyTrends (GET /feedback/surveys/{survey_id}/trends?bucket) read score by day, week or month
|
|
9
|
+
* A score the API leaves out (too few answers) shows a dash, never 0.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
|
+
import { BarList } from "@/g8/components/screen/bar-list";
|
|
17
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
18
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { plural } from "@/g8/lib/plural";
|
|
22
|
+
import { BUCKETS, isNps, percent, scoreText } from "../lib/surveys-types";
|
|
23
|
+
import type { Bucket, Summary, Survey, Trends } from "../lib/surveys-types";
|
|
24
|
+
|
|
25
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
26
|
+
|
|
27
|
+
export function SurveyResults({ survey }: { survey: Pick<Survey, "id" | "type" | "name"> }) {
|
|
28
|
+
const [bucket, setBucket] = useState<Bucket>("week");
|
|
29
|
+
const summary = useG8Query((signal) => g8Get<Summary>(`/feedback/surveys/${encodeURIComponent(survey.id)}/summary`, { bucket }, { signal }).then((r) => r.data), [survey.id, bucket], { key: ["surveys", "summary", survey.id, bucket] });
|
|
30
|
+
const trends = useG8Query((signal) => g8Get<Trends>(`/feedback/surveys/${encodeURIComponent(survey.id)}/trends`, { bucket }, { signal }).then((r) => r.data), [survey.id, bucket], { key: ["surveys", "trends", survey.id, bucket] });
|
|
31
|
+
const nps = isNps(survey);
|
|
32
|
+
|
|
33
|
+
if (summary.error) return <ApiError error={summary.error} title="Could not load the results" onRetry={() => void summary.refetch()} />;
|
|
34
|
+
if (!summary.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
|
|
35
|
+
const s = summary.data;
|
|
36
|
+
const bars = trends.data?.bars ?? s.time_series ?? [];
|
|
37
|
+
const split = nps
|
|
38
|
+
? [{ label: "Promoters (9 to 10)", value: s.promoters, tone: "positive" as const }, { label: "Passives (7 to 8)", value: s.passives }, { label: "Detractors (0 to 6)", value: s.detractors, tone: "negative" as const }]
|
|
39
|
+
: [{ label: "Satisfied (4 to 5)", value: s.satisfied ?? 0, tone: "positive" as const }, { label: "Neutral (3)", value: s.neutral ?? 0 }, { label: "Dissatisfied (1 to 2)", value: s.dissatisfied ?? 0, tone: "negative" as const }];
|
|
40
|
+
return (
|
|
41
|
+
<div className="grid gap-4">
|
|
42
|
+
<div className="flex items-center justify-end gap-2">
|
|
43
|
+
<label htmlFor="g8-svr-bucket" className="text-muted-foreground text-sm">Trend by</label>
|
|
44
|
+
<select id="g8-svr-bucket" aria-label="Trend bucket" className={SELECT} value={bucket} onChange={(e) => setBucket(e.target.value as Bucket)}>
|
|
45
|
+
{BUCKETS.map((b) => <option key={b.value} value={b.value}>{b.label}</option>)}
|
|
46
|
+
</select>
|
|
47
|
+
</div>
|
|
48
|
+
<StatStrip
|
|
49
|
+
items={[
|
|
50
|
+
nps ? { label: "NPS", value: scoreText(s.nps_score, 0), hint: "Promoters minus detractors, from -100 to 100. A dash means there are too few answers." } : { label: "Average CSAT", value: scoreText(s.csat_avg, 1), hint: "Mean score from 1 to 5." },
|
|
51
|
+
{ label: "Answers", value: s.total_respondents.toLocaleString() },
|
|
52
|
+
{ label: "Response rate", value: percent(s.response_rate.responded, s.response_rate.sent), hint: `${s.response_rate.responded.toLocaleString()} of ${s.response_rate.sent.toLocaleString()} people asked answered.` },
|
|
53
|
+
]}
|
|
54
|
+
/>
|
|
55
|
+
<SectionCard title="Who answered what" description={s.total_respondents ? plural(s.total_respondents, "answer") : undefined}>
|
|
56
|
+
{s.total_respondents === 0 ? <EmptyState title="No answers yet" description="Answers appear here as soon as people respond." /> : <BarList items={split} formatValue={(v) => v.toLocaleString()} emphasizeFirst={false} />}
|
|
57
|
+
</SectionCard>
|
|
58
|
+
<SectionCard title="Over time">
|
|
59
|
+
{trends.error ? <ApiError error={trends.error} title="Could not load the trend" onRetry={() => void trends.refetch()} /> : bars.length === 0 ? <p className="text-muted-foreground text-sm">No answers in this period yet.</p> : (
|
|
60
|
+
<table className="w-full text-sm">
|
|
61
|
+
<thead><tr className="text-muted-foreground text-left text-xs"><th className="py-1 font-normal">Period</th><th className="py-1 font-normal">Answers</th><th className="py-1 font-normal">{nps ? "NPS" : "Average"}</th></tr></thead>
|
|
62
|
+
<tbody>
|
|
63
|
+
{bars.map((b) => (
|
|
64
|
+
<tr key={b.period} className="border-t"><td className="py-1.5">{b.period}</td><td className="py-1.5">{b.total.toLocaleString()}</td><td className="py-1.5">{nps ? scoreText(b.nps_score, 0) : scoreText(b.csat_avg, 1)}</td></tr>
|
|
65
|
+
))}
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
)}
|
|
69
|
+
</SectionCard>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SurveySendHistory: every send of one survey with its status, audience, how many people were reached and how many answered.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getSurveySends (GET /feedback/surveys/{survey_id}/sends) read one unpaged read: the API returns the full history
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RefreshCw } from "lucide-react";
|
|
11
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
12
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
13
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
14
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
15
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
16
|
+
import type { Tone } from "@/g8/components/screen/status-pill";
|
|
17
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { plural } from "@/g8/lib/plural";
|
|
20
|
+
import { percent, when } from "../lib/surveys-types";
|
|
21
|
+
import type { SendRow } from "../lib/surveys-types";
|
|
22
|
+
|
|
23
|
+
const tone = (s: string): Tone => (s === "sent" ? "positive" : s === "failed" ? "negative" : s === "draft" ? "neutral" : "info");
|
|
24
|
+
|
|
25
|
+
export function SurveySendHistory({ surveyId }: { surveyId: string }) {
|
|
26
|
+
const sends = useG8Query(
|
|
27
|
+
(signal) => g8Get<{ sends?: SendRow[] }>(`/feedback/surveys/${encodeURIComponent(surveyId)}/sends`, {}, { signal }).then((r) => r.data?.sends ?? []),
|
|
28
|
+
[surveyId],
|
|
29
|
+
{ key: ["surveys", "sends", surveyId] },
|
|
30
|
+
);
|
|
31
|
+
const rows = sends.data ?? [];
|
|
32
|
+
return (
|
|
33
|
+
<SectionCard
|
|
34
|
+
title="Send history"
|
|
35
|
+
description={sends.data ? `${plural(rows.length, "send")}, newest first. This is the complete history.` : undefined}
|
|
36
|
+
actions={<Button size="sm" variant="outline" loading={sends.loading && Boolean(sends.data)} onClick={() => void sends.refetch()}><RefreshCw className="size-4" />Refresh</Button>}
|
|
37
|
+
>
|
|
38
|
+
{sends.error ? <ApiError error={sends.error} title="Could not load the send history" onRetry={() => void sends.refetch()} /> : sends.loading && !sends.data ? (
|
|
39
|
+
<div aria-busy="true" className="grid gap-2"><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>
|
|
40
|
+
) : rows.length === 0 ? <EmptyState title="Not sent yet" description="Sends appear here once you send this survey to a list." /> : (
|
|
41
|
+
<ul className="grid gap-2">
|
|
42
|
+
{[...rows].sort((a, b) => (b.created_at ?? "").localeCompare(a.created_at ?? "")).map((s) => (
|
|
43
|
+
<li key={s.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 text-sm">
|
|
44
|
+
<div className="min-w-0">
|
|
45
|
+
<p className="truncate font-medium">{s.audience_name || (s.trigger_kind ? `Automatic (${s.trigger_kind.replace(/_/g, " ")})` : "Audience list")}</p>
|
|
46
|
+
<p className="text-muted-foreground text-xs">
|
|
47
|
+
{s.scheduled_at ? `Scheduled ${when(s.scheduled_at)}` : when(s.created_at) || "Not dated"} · {plural(s.recipient_count ?? 0, "recipient")} · {s.responded_count ?? 0} answered ({percent(s.responded_count, s.recipient_count)})
|
|
48
|
+
{s.delivery_breakdown ? ` · ${s.delivery_breakdown.delivered ?? 0} delivered, ${s.delivery_breakdown.bounced ?? 0} bounced` : ""}
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<StatusPill tone={tone(s.status)}>{s.status}</StatusPill>
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ul>
|
|
55
|
+
)}
|
|
56
|
+
</SectionCard>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SurveySend: send a survey to an audience list now or later, then follow the send; the history of earlier sends sits below.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. listSurveySendingDomains (GET /feedback/sending-domains) read the domains a sender address may use
|
|
8
|
+
* 2. listLists (GET /lists), through ListSelect read the audience
|
|
9
|
+
* 3. sendSurvey (POST /feedback/surveys/{survey_id}/send) external REAL MAIL to real people: ConfirmSpendDialog, sized from the chosen list
|
|
10
|
+
* 4. getSurveySend (GET /feedback/sends/{send_id}) read polled after the send is accepted
|
|
11
|
+
* The confirmation shows the audience size read from the list itself ("up to N": people who cannot be reached, such as
|
|
12
|
+
* unsubscribed or suppressed ones, are dropped by the API). An in-app survey is not sent: it shows inside your product.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useRef, useState } from "react";
|
|
16
|
+
import { Send } from "lucide-react";
|
|
17
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
22
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
23
|
+
import { ListSelect } from "@/g8/crm/components/list-select";
|
|
24
|
+
import type { ListSelectOption } from "@/g8/crm/components/list-select";
|
|
25
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
29
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
30
|
+
import { plural } from "@/g8/lib/plural";
|
|
31
|
+
import { EMAIL_RE, SEND_CHANNELS, audienceSize, domainOf, scheduleIso, when } from "../lib/surveys-types";
|
|
32
|
+
import type { SendAccepted, SendChannel, SendDetail, SendingDomain, Survey } from "../lib/surveys-types";
|
|
33
|
+
import { SurveySendHistory } from "./survey-send-history";
|
|
34
|
+
|
|
35
|
+
export interface SurveySendProps {
|
|
36
|
+
survey: Survey;
|
|
37
|
+
/** Called after a send is accepted, so a parent can refresh its numbers. */
|
|
38
|
+
onSent?: (accepted: SendAccepted) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
|
|
42
|
+
const SETTLED = new Set(["sent", "failed", "scheduled"]);
|
|
43
|
+
|
|
44
|
+
export function SurveySend({ survey, onSent }: SurveySendProps) {
|
|
45
|
+
const brand = useBrand();
|
|
46
|
+
const [list, setList] = useState<ListSelectOption | null>(null);
|
|
47
|
+
const [channel, setChannel] = useState<SendChannel>(survey.delivery_channel === "bridge" ? "bridge" : "email");
|
|
48
|
+
const [senderEmail, setSenderEmail] = useState("");
|
|
49
|
+
const [senderName, setSenderName] = useState("");
|
|
50
|
+
const [replyTo, setReplyTo] = useState("");
|
|
51
|
+
const [subject, setSubject] = useState(survey.name);
|
|
52
|
+
const [at, setAt] = useState("");
|
|
53
|
+
const [confirm, setConfirm] = useState(false);
|
|
54
|
+
const [accepted, setAccepted] = useState<SendAccepted | null>(null);
|
|
55
|
+
const [historyKey, setHistoryKey] = useState(0);
|
|
56
|
+
// The poll reads the id from a ref: start() runs its first tick before React re-renders with the new state.
|
|
57
|
+
const sendId = useRef<number | null>(null);
|
|
58
|
+
|
|
59
|
+
const domains = useG8Query((signal) => g8Get<SendingDomain[]>("/feedback/sending-domains", {}, { signal }).then((r) => r.data ?? []), [], { key: ["surveys", "domains", "verified"] });
|
|
60
|
+
const status = useJobPoll<SendDetail>((signal) => g8Get<SendDetail>(`/feedback/sends/${sendId.current}`, {}, { signal }).then((r) => r.data), {
|
|
61
|
+
isTerminal: (s) => SETTLED.has(s.status),
|
|
62
|
+
enabled: false,
|
|
63
|
+
onDone: () => setHistoryKey((n) => n + 1),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const send = useG8Mutation(async (): Promise<SendAccepted> => {
|
|
67
|
+
const body: Record<string, unknown> = { audience_ref: String(list!.id), channel };
|
|
68
|
+
if (channel === "email") {
|
|
69
|
+
body.sender_email = senderEmail.trim();
|
|
70
|
+
body.subject = subject.trim();
|
|
71
|
+
if (senderName.trim()) body.sender_name = senderName.trim();
|
|
72
|
+
if (replyTo.trim()) body.reply_to = replyTo.trim();
|
|
73
|
+
}
|
|
74
|
+
const when_ = scheduleIso(at);
|
|
75
|
+
if (when_) body.scheduled_at = when_;
|
|
76
|
+
return (await g8Post<SendAccepted>(`/feedback/surveys/${encodeURIComponent(survey.id)}/send`, body)).data;
|
|
77
|
+
}, {
|
|
78
|
+
onSuccess: (r) => { sendId.current = r.send_id; setConfirm(false); setAccepted(r); setHistoryKey((n) => n + 1); onSent?.(r); status.start(); },
|
|
79
|
+
invalidates: [["surveys", "summary"]],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (survey.delivery_channel === "in_app") {
|
|
83
|
+
return <EmptyState title="This survey is shown inside your product" description={`An in-app survey is not sent: the widget in ${brand.linkLabel} shows it to signed-in team members, and their answers appear under Responses. Change the survey to email or chat to send it to contacts.`} />;
|
|
84
|
+
}
|
|
85
|
+
const verified = (domains.data ?? []).filter((d) => d.status === "verified");
|
|
86
|
+
const domainOk = channel === "bridge" || (EMAIL_RE.test(senderEmail.trim()) && verified.some((d) => d.domain === domainOf(senderEmail.trim())));
|
|
87
|
+
const emailOk = channel === "bridge" || (domainOk && subject.trim() !== "" && (replyTo.trim() === "" || EMAIL_RE.test(replyTo.trim())));
|
|
88
|
+
const ready = Boolean(list) && emailOk && survey.is_active && (at === "" || (scheduleIso(at) !== undefined && new Date(at).getTime() > Date.now()));
|
|
89
|
+
const size = audienceSize(list);
|
|
90
|
+
const sendsLater = Boolean(scheduleIso(at));
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="grid gap-4">
|
|
94
|
+
{!survey.is_active ? <p role="note" className="text-warning-foreground text-sm">This survey is closed. Reopen it before sending.</p> : null}
|
|
95
|
+
<SectionCard title="Send this survey" description="Choose who gets it. Nothing is sent until you confirm.">
|
|
96
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (ready) setConfirm(true); }}>
|
|
97
|
+
<div>
|
|
98
|
+
<Label htmlFor="g8-svs-list" className="mb-1">Audience list</Label>
|
|
99
|
+
<ListSelect id="g8-svs-list" label="Audience list" emptyOption="Choose a list" value={list?.id ?? ""} valueLabel={list?.title} selectClassName={SELECT} onChange={setList} />
|
|
100
|
+
{list && size === null ? <p className="text-muted-foreground mt-1 text-xs">The list does not report its size; the confirmation says so.</p> : null}
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<Label htmlFor="g8-svs-channel" className="mb-1">Send as</Label>
|
|
104
|
+
<select id="g8-svs-channel" className={SELECT} value={channel} onChange={(e) => setChannel(e.target.value as SendChannel)}>
|
|
105
|
+
{SEND_CHANNELS.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}
|
|
106
|
+
</select>
|
|
107
|
+
</div>
|
|
108
|
+
{channel === "email" ? (
|
|
109
|
+
<>
|
|
110
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
111
|
+
<div>
|
|
112
|
+
<Label htmlFor="g8-svs-from" className="mb-1">From address</Label>
|
|
113
|
+
<Input id="g8-svs-from" type="email" value={senderEmail} placeholder={verified[0] ? `survey@${verified[0].domain}` : "survey@yourdomain.com"} onChange={(e) => setSenderEmail(e.target.value)} />
|
|
114
|
+
</div>
|
|
115
|
+
<div><Label htmlFor="g8-svs-name" className="mb-1">From name (optional)</Label><Input id="g8-svs-name" value={senderName} onChange={(e) => setSenderName(e.target.value)} /></div>
|
|
116
|
+
</div>
|
|
117
|
+
{domains.loading && !domains.data ? <Skeleton className="h-4 w-48" /> : domains.error ? <ApiError error={domains.error} title="Could not load sending domains" onRetry={() => void domains.refetch()} /> : verified.length === 0 ? (
|
|
118
|
+
<p role="note" className="text-warning-foreground text-xs">No sending domain is verified yet. Verify one under Sending domains before sending email.</p>
|
|
119
|
+
) : senderEmail.trim() && !domainOk ? (
|
|
120
|
+
<p role="alert" className="text-error text-xs">The address must be on a verified domain: {verified.map((d) => d.domain).join(", ")}.</p>
|
|
121
|
+
) : <p className="text-muted-foreground text-xs">Verified domains: {verified.map((d) => d.domain).join(", ")}.</p>}
|
|
122
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
123
|
+
<div><Label htmlFor="g8-svs-subject" className="mb-1">Subject</Label><Input id="g8-svs-subject" value={subject} onChange={(e) => setSubject(e.target.value)} /></div>
|
|
124
|
+
<div><Label htmlFor="g8-svs-reply" className="mb-1">Reply-to (optional)</Label><Input id="g8-svs-reply" type="email" value={replyTo} onChange={(e) => setReplyTo(e.target.value)} /></div>
|
|
125
|
+
</div>
|
|
126
|
+
</>
|
|
127
|
+
) : null}
|
|
128
|
+
<div className="sm:w-64"><Label htmlFor="g8-svs-at" className="mb-1">Send later (optional)</Label><Input id="g8-svs-at" type="datetime-local" value={at} onChange={(e) => setAt(e.target.value)} /></div>
|
|
129
|
+
{at !== "" && scheduleIso(at) !== undefined && new Date(at).getTime() <= Date.now() ? <p role="alert" className="text-error text-xs">Pick a time in the future, or clear it to send now.</p> : null}
|
|
130
|
+
<div><Button type="submit" disabled={!ready}><Send className="size-4" />{sendsLater ? "Schedule send" : "Send survey"}</Button></div>
|
|
131
|
+
</form>
|
|
132
|
+
</SectionCard>
|
|
133
|
+
{accepted ? (
|
|
134
|
+
<SectionCard title="Last send">
|
|
135
|
+
<p className="flex flex-wrap items-center gap-2 text-sm" role="status">
|
|
136
|
+
<StatusPill tone={status.status?.status === "failed" ? "negative" : status.status?.status === "sent" ? "positive" : "info"}>{status.status?.status ?? accepted.status}</StatusPill>
|
|
137
|
+
{plural(status.status?.recipient_count ?? accepted.recipient_count, "recipient")}
|
|
138
|
+
{status.status?.scheduled_at ? `, scheduled for ${when(status.status.scheduled_at)}` : ""}
|
|
139
|
+
</p>
|
|
140
|
+
{status.error ? <ApiError error={status.error} title="Could not read the send status" onRetry={() => status.start()} /> : null}
|
|
141
|
+
</SectionCard>
|
|
142
|
+
) : null}
|
|
143
|
+
<SurveySendHistory key={historyKey} surveyId={survey.id} />
|
|
144
|
+
<ConfirmSpendDialog
|
|
145
|
+
open={confirm}
|
|
146
|
+
tier="external"
|
|
147
|
+
title={`${sendsLater ? "Schedule" : "Send"} "${survey.name}" to ${list?.title ?? "this list"}?`}
|
|
148
|
+
recipients={size ?? undefined}
|
|
149
|
+
description={(
|
|
150
|
+
<>
|
|
151
|
+
<p>{size === null ? "The list does not report its size." : `Up to ${plural(size, "person", "people")} will be asked.`} People who unsubscribed or are suppressed are skipped. {sendsLater ? `It goes out on ${when(scheduleIso(at))}.` : "It goes out now and cannot be recalled."}</p>
|
|
152
|
+
{send.error ? <ApiError error={send.error} title="The send was refused" /> : null}
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
confirmLabel={sendsLater ? "Schedule" : "Send"}
|
|
156
|
+
confirming={send.loading}
|
|
157
|
+
onCancel={() => setConfirm(false)}
|
|
158
|
+
onConfirm={() => void send.mutate(undefined)}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SurveyTriggers: automatic sends for one survey. A trigger mails an audience on a schedule, when someone joins a list, or
|
|
5
|
+
* when a record reaches a stage, without anyone pressing Send.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listSurveyTriggers (GET /feedback/surveys/{survey_id}/triggers) read
|
|
9
|
+
* 2. listSurveySendingDomains (GET /feedback/sending-domains) read the sender must be on a verified domain
|
|
10
|
+
* 3. listLists (GET /lists), through ListSelect read
|
|
11
|
+
* 4. createSurveyTrigger (POST /feedback/surveys/{survey_id}/triggers) write confirmed here (external): it will mail people on its own
|
|
12
|
+
* 5. deleteSurveyTrigger (DELETE /feedback/surveys/{survey_id}/triggers/{id}) destructive ConfirmSpendDialog
|
|
13
|
+
* The API has no trigger update or pause: to change one, delete it and create it again.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Plus, Trash2, Zap } from "lucide-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 { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
23
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
24
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { useAllLists } from "@/g8/crm/hooks/use-all-lists";
|
|
26
|
+
import { ListSelect } from "@/g8/crm/components/list-select";
|
|
27
|
+
import type { ListSelectOption } from "@/g8/crm/components/list-select";
|
|
28
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
30
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
31
|
+
import { plural } from "@/g8/lib/plural";
|
|
32
|
+
import { CADENCE_DAYS, EMAIL_RE, TRIGGER_KINDS, audienceSize, domainOf } from "../lib/surveys-types";
|
|
33
|
+
import type { SendingDomain, Survey, Trigger, TriggerKind } from "../lib/surveys-types";
|
|
34
|
+
|
|
35
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
36
|
+
const kindLabel = (k: string): string => TRIGGER_KINDS.find((t) => t.value === k)?.label ?? k;
|
|
37
|
+
|
|
38
|
+
export function SurveyTriggers({ survey }: { survey: Survey }) {
|
|
39
|
+
const [creating, setCreating] = useState(false);
|
|
40
|
+
const [confirmNew, setConfirmNew] = useState(false);
|
|
41
|
+
const [toDelete, setToDelete] = useState<Trigger | null>(null);
|
|
42
|
+
const [kind, setKind] = useState<TriggerKind>("scheduled");
|
|
43
|
+
const [list, setList] = useState<ListSelectOption | null>(null);
|
|
44
|
+
const [cadence, setCadence] = useState<number>(90);
|
|
45
|
+
const [stage, setStage] = useState("");
|
|
46
|
+
const [senderEmail, setSenderEmail] = useState("");
|
|
47
|
+
const [senderName, setSenderName] = useState("");
|
|
48
|
+
const [replyTo, setReplyTo] = useState("");
|
|
49
|
+
const [subject, setSubject] = useState(survey.name);
|
|
50
|
+
|
|
51
|
+
const triggers = useG8Query((signal) => g8Get<Trigger[]>(`/feedback/surveys/${encodeURIComponent(survey.id)}/triggers`, {}, { signal }).then((r) => r.data ?? []), [survey.id], { key: ["surveys", "triggers", survey.id] });
|
|
52
|
+
const domains = useG8Query((signal) => g8Get<SendingDomain[]>("/feedback/sending-domains", {}, { signal }).then((r) => r.data ?? []), [], { key: ["surveys", "domains", "verified"], enabled: creating });
|
|
53
|
+
|
|
54
|
+
const lists = useAllLists([]);
|
|
55
|
+
const needsList = kind !== "stage_entry";
|
|
56
|
+
const verified = (domains.data ?? []).filter((d) => d.status === "verified");
|
|
57
|
+
const senderOk = EMAIL_RE.test(senderEmail.trim()) && verified.some((d) => d.domain === domainOf(senderEmail.trim()));
|
|
58
|
+
const valid = senderOk && subject.trim() !== "" && (!needsList || Boolean(list)) && (kind !== "stage_entry" || stage.trim() !== "") && (replyTo.trim() === "" || EMAIL_RE.test(replyTo.trim()));
|
|
59
|
+
const reset = (): void => { setCreating(false); setConfirmNew(false); setList(null); setStage(""); setSenderEmail(""); setSenderName(""); setReplyTo(""); setSubject(survey.name); };
|
|
60
|
+
|
|
61
|
+
const create = useG8Mutation(async (): Promise<Trigger> => {
|
|
62
|
+
// Only the fields this kind uses: a stage value on a scheduled trigger would be stored and misread later.
|
|
63
|
+
const body: Record<string, unknown> = { trigger_kind: kind, sender_email: senderEmail.trim(), subject: subject.trim() };
|
|
64
|
+
if (senderName.trim()) body.sender_name = senderName.trim();
|
|
65
|
+
if (replyTo.trim()) body.reply_to = replyTo.trim();
|
|
66
|
+
if (needsList) body.audience_ref = String(list!.id);
|
|
67
|
+
if (kind === "scheduled") body.cadence_days = cadence;
|
|
68
|
+
if (kind === "stage_entry") body.stage_value = stage.trim();
|
|
69
|
+
return (await g8Post<Trigger>(`/feedback/surveys/${encodeURIComponent(survey.id)}/triggers`, body)).data;
|
|
70
|
+
}, { onSuccess: reset, invalidates: [["surveys", "triggers", survey.id]] });
|
|
71
|
+
const remove = useG8Mutation((t: Trigger) => g8Delete(`/feedback/surveys/${encodeURIComponent(survey.id)}/triggers/${encodeURIComponent(t.id)}`), {
|
|
72
|
+
onSuccess: () => setToDelete(null),
|
|
73
|
+
invalidates: [["surveys", "triggers", survey.id]],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const rows = triggers.data ?? [];
|
|
77
|
+
const size = audienceSize(list);
|
|
78
|
+
return (
|
|
79
|
+
<SectionCard
|
|
80
|
+
title="Automatic sends"
|
|
81
|
+
description="Mail this survey without pressing Send."
|
|
82
|
+
actions={<Button size="sm" onClick={() => setCreating(true)} disabled={survey.delivery_channel !== "email"}><Plus className="size-4" />New automatic send</Button>}
|
|
83
|
+
>
|
|
84
|
+
{survey.delivery_channel !== "email" ? <p className="text-muted-foreground mb-2 text-xs">Automatic sends go out by email. Switch this survey to email to add one.</p> : null}
|
|
85
|
+
{triggers.error ? <ApiError error={triggers.error} title="Could not load automatic sends" onRetry={() => void triggers.refetch()} /> : triggers.loading && !triggers.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : rows.length === 0 ? (
|
|
86
|
+
<EmptyState icon={<Zap className="size-8" />} title="No automatic sends" description="Add one to ask a list every 30, 60 or 90 days, or whenever someone joins a list or reaches a stage." />
|
|
87
|
+
) : (
|
|
88
|
+
<ul className="grid gap-2">
|
|
89
|
+
{rows.map((t) => (
|
|
90
|
+
<li key={t.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 text-sm">
|
|
91
|
+
<div className="min-w-0">
|
|
92
|
+
<p className="font-medium">{kindLabel(t.trigger_kind)}{t.cadence_days ? `, every ${plural(t.cadence_days, "day")}` : ""}{t.stage_value ? `: ${t.stage_value}` : ""}</p>
|
|
93
|
+
<p className="text-muted-foreground truncate text-xs">From {t.sender_email} · {t.subject}{t.audience_ref ? ` · ${lists.items.find((l) => String(l.id) === t.audience_ref)?.title ?? "an audience list"}` : ""}</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex items-center gap-2">
|
|
96
|
+
<StatusPill tone={t.is_active ? "positive" : "neutral"}>{t.is_active ? "active" : "inactive"}</StatusPill>
|
|
97
|
+
<Button size="sm" variant="ghost" aria-label={`Remove automatic send ${kindLabel(t.trigger_kind)}`} onClick={() => setToDelete(t)}><Trash2 className="size-4" /></Button>
|
|
98
|
+
</div>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
)}
|
|
103
|
+
<FormSheet
|
|
104
|
+
open={creating}
|
|
105
|
+
onOpenChange={(o) => { if (!o) reset(); else setCreating(true); }}
|
|
106
|
+
title="New automatic send"
|
|
107
|
+
description="Once saved, this mails people on its own. You confirm it first."
|
|
108
|
+
footer={<Button disabled={!valid} onClick={() => setConfirmNew(true)}>Add automatic send</Button>}
|
|
109
|
+
>
|
|
110
|
+
<div className="grid gap-3">
|
|
111
|
+
<div>
|
|
112
|
+
<Label htmlFor="g8-svt-kind" className="mb-1">When</Label>
|
|
113
|
+
<select id="g8-svt-kind" className={SELECT} value={kind} onChange={(e) => setKind(e.target.value as TriggerKind)}>
|
|
114
|
+
{TRIGGER_KINDS.map((k) => <option key={k.value} value={k.value}>{k.label}</option>)}
|
|
115
|
+
</select>
|
|
116
|
+
<p className="text-muted-foreground mt-1 text-xs">{TRIGGER_KINDS.find((k) => k.value === kind)?.hint}</p>
|
|
117
|
+
</div>
|
|
118
|
+
{needsList ? (
|
|
119
|
+
<div>
|
|
120
|
+
<Label htmlFor="g8-svt-list" className="mb-1">{kind === "list_entry" ? "Watched list" : "Audience list"}</Label>
|
|
121
|
+
<ListSelect id="g8-svt-list" label={kind === "list_entry" ? "Watched list" : "Audience list"} emptyOption="Choose a list" value={list?.id ?? ""} valueLabel={list?.title} selectClassName={SELECT} onChange={setList} />
|
|
122
|
+
</div>
|
|
123
|
+
) : null}
|
|
124
|
+
{kind === "scheduled" ? (
|
|
125
|
+
<div>
|
|
126
|
+
<Label htmlFor="g8-svt-cadence" className="mb-1">Repeat every</Label>
|
|
127
|
+
<select id="g8-svt-cadence" className={SELECT} value={cadence} onChange={(e) => setCadence(Number(e.target.value))}>
|
|
128
|
+
{CADENCE_DAYS.map((d) => <option key={d} value={d}>{d} days</option>)}
|
|
129
|
+
</select>
|
|
130
|
+
</div>
|
|
131
|
+
) : null}
|
|
132
|
+
{kind === "stage_entry" ? <div><Label htmlFor="g8-svt-stage" className="mb-1">Stage</Label><Input id="g8-svt-stage" value={stage} placeholder="closed-won" onChange={(e) => setStage(e.target.value)} /></div> : null}
|
|
133
|
+
<div><Label htmlFor="g8-svt-from" className="mb-1">From address</Label><Input id="g8-svt-from" type="email" value={senderEmail} onChange={(e) => setSenderEmail(e.target.value)} /></div>
|
|
134
|
+
{domains.data && senderEmail.trim() && !senderOk ? <p role="alert" className="text-error text-xs">{verified.length ? `Use an address on a verified domain: ${verified.map((d) => d.domain).join(", ")}.` : "No sending domain is verified yet. Verify one under Sending domains."}</p> : null}
|
|
135
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
136
|
+
<div><Label htmlFor="g8-svt-name" className="mb-1">From name (optional)</Label><Input id="g8-svt-name" value={senderName} onChange={(e) => setSenderName(e.target.value)} /></div>
|
|
137
|
+
<div><Label htmlFor="g8-svt-reply" className="mb-1">Reply-to (optional)</Label><Input id="g8-svt-reply" type="email" value={replyTo} onChange={(e) => setReplyTo(e.target.value)} /></div>
|
|
138
|
+
</div>
|
|
139
|
+
<div><Label htmlFor="g8-svt-subject" className="mb-1">Subject</Label><Input id="g8-svt-subject" value={subject} onChange={(e) => setSubject(e.target.value)} /></div>
|
|
140
|
+
</div>
|
|
141
|
+
</FormSheet>
|
|
142
|
+
<ConfirmSpendDialog
|
|
143
|
+
open={confirmNew}
|
|
144
|
+
tier="external"
|
|
145
|
+
title={`Add an automatic send for "${survey.name}"?`}
|
|
146
|
+
recipients={needsList ? (size ?? undefined) : undefined}
|
|
147
|
+
description={<><p>{kindLabel(kind)}: this survey is emailed from {senderEmail.trim() || "the sender"} without anyone pressing Send.{needsList && size !== null ? ` The list has ${plural(size, "person", "people")} now.` : ""}</p>{create.error ? <ApiError error={create.error} title="The automatic send was refused" /> : null}</>}
|
|
148
|
+
confirmLabel="Add"
|
|
149
|
+
confirming={create.loading}
|
|
150
|
+
onCancel={() => setConfirmNew(false)}
|
|
151
|
+
onConfirm={() => void create.mutate(undefined)}
|
|
152
|
+
/>
|
|
153
|
+
<ConfirmSpendDialog
|
|
154
|
+
open={toDelete !== null}
|
|
155
|
+
tier="destructive"
|
|
156
|
+
title={`Remove this automatic send?`}
|
|
157
|
+
affected={1}
|
|
158
|
+
description={<><p>{toDelete ? `${kindLabel(toDelete.trigger_kind)} stops. Surveys already sent and their answers stay.` : ""}</p>{remove.error ? <ApiError error={remove.error} title="Could not remove it" /> : null}</>}
|
|
159
|
+
confirmLabel="Remove"
|
|
160
|
+
confirming={remove.loading}
|
|
161
|
+
onCancel={() => setToDelete(null)}
|
|
162
|
+
onConfirm={() => { if (toDelete) void remove.mutate(toDelete); }}
|
|
163
|
+
/>
|
|
164
|
+
</SectionCard>
|
|
165
|
+
);
|
|
166
|
+
}
|