@graph8/sdk 0.370.0 → 0.374.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 +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -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 +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- 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 +53 -11
- 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/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -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 +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- 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 +165 -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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- 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 +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- 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 +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- 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 +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- 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 +57 -20
- 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 +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -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 +20 -1
- 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 +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- 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 +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- 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 +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- 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/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- 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 +38 -13
- 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/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- 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-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/hooks/use-landing-page-paths.ts +60 -0
- 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/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- 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 +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- 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 +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -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-screen.tsx +33 -6
- 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/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- 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 +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- 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 +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- 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 +34 -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 +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- 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/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- 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 +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- 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 +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -34,9 +34,13 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
|
34
34
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
35
35
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
36
36
|
import { ApiError } from "@/g8/components/api-error";
|
|
37
|
+
import { ListSelect } from "@/g8/crm/components/list-select";
|
|
38
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
37
39
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
38
40
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
41
|
+
import { StepAiAssist } from "./step-ai-assist";
|
|
39
42
|
import { StepEditor } from "./step-editor";
|
|
43
|
+
import { StepTemplatePicker } from "./step-templates";
|
|
40
44
|
import {
|
|
41
45
|
fromApiStep,
|
|
42
46
|
KIND_LABEL,
|
|
@@ -63,6 +67,10 @@ export interface SequenceBuilderProps {
|
|
|
63
67
|
/** Optional owner email sent as `user_email` on create. */
|
|
64
68
|
ownerEmail?: string;
|
|
65
69
|
onSaved?: (sequenceId: string) => void;
|
|
70
|
+
/** New sequence only: start with this name (for example from a template). */
|
|
71
|
+
initialName?: string;
|
|
72
|
+
/** New sequence only: start with these unsaved steps (for example from a template). Sender and audience are still asked for. */
|
|
73
|
+
initialSteps?: DraftStep[];
|
|
66
74
|
className?: string;
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -102,11 +110,8 @@ const PHONE_KEYS = ["phone_numbers", "active_phone_numbers", "drafted_phone_numb
|
|
|
102
110
|
const LINKEDIN_KEYS = ["linkedin_accounts", "active_linkedin_accounts", "drafted_linkedin_accounts"];
|
|
103
111
|
const phoneFits = (p: PhoneOption, type: "ai" | "dialer") => !p.inbound_type || p.inbound_type === type;
|
|
104
112
|
const linkedinLabel = (a: LinkedinOption) => a.name || a.display_name || a.public_identifier || a.email || `Account ${a.id}`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
title: string;
|
|
108
|
-
contact_count?: number | null;
|
|
109
|
-
}
|
|
113
|
+
/** The mailboxes API has no paging: a full read is announced under the sender dropdown. */
|
|
114
|
+
const MAILBOX_LIMIT = 200;
|
|
110
115
|
type Pending = { kind: "create" } | { kind: "update" | "delete"; step: DraftStep };
|
|
111
116
|
|
|
112
117
|
function describe(p: Pending, live: boolean): { tier: ConfirmTier; title: string; description: string; label: string } {
|
|
@@ -134,16 +139,17 @@ function describe(p: Pending, live: boolean): { tier: ConfirmTier; title: string
|
|
|
134
139
|
};
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved, className }: SequenceBuilderProps) {
|
|
142
|
+
export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved, initialName, initialSteps, className }: SequenceBuilderProps) {
|
|
138
143
|
const brand = useBrand();
|
|
139
144
|
const editing = Boolean(sequenceId);
|
|
140
|
-
const [name, setName] = useState("");
|
|
145
|
+
const [name, setName] = useState(initialName ?? "");
|
|
141
146
|
const [mailboxId, setMailboxId] = useState("");
|
|
142
147
|
const [aiPhoneId, setAiPhoneId] = useState("");
|
|
143
148
|
const [dialerPhoneId, setDialerPhoneId] = useState("");
|
|
144
149
|
const [linkedinId, setLinkedinId] = useState("");
|
|
145
150
|
const [listId, setListId] = useState(defaultListId ? String(defaultListId) : "");
|
|
146
|
-
const [steps, setSteps] = useState<DraftStep[]>([]);
|
|
151
|
+
const [steps, setSteps] = useState<DraftStep[]>(() => (sequenceId ? [] : (initialSteps ?? [])));
|
|
152
|
+
const [pickingTemplate, setPickingTemplate] = useState(false);
|
|
147
153
|
const [loadedFor, setLoadedFor] = useState<string | null>(null);
|
|
148
154
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
149
155
|
const [saved, setSaved] = useState<string | null>(null);
|
|
@@ -157,8 +163,7 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
157
163
|
// Senders already attached in edit mode by an earlier (failed) append: never attach them twice.
|
|
158
164
|
const attached = useRef<Set<string>>(new Set());
|
|
159
165
|
|
|
160
|
-
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit:
|
|
161
|
-
const lists = useG8Query((signal) => g8Get("/lists", { limit: 100 }, { signal }), [], { enabled: !editing });
|
|
166
|
+
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT }, { signal }), [], { enabled: !editing });
|
|
162
167
|
const existing = useG8Query(
|
|
163
168
|
async (signal) => {
|
|
164
169
|
const [seq, list] = await Promise.all([
|
|
@@ -323,15 +328,17 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
323
328
|
);
|
|
324
329
|
})}
|
|
325
330
|
</select>
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
<ListSelect
|
|
332
|
+
label="Audience list"
|
|
333
|
+
emptyOption="Audience list (required)"
|
|
334
|
+
optionLabel={(l) => `${l.title}${l.contact_count != null ? ` (${l.contact_count})` : ""}`}
|
|
335
|
+
pageSize={100}
|
|
336
|
+
value={listId}
|
|
337
|
+
valueLabel={defaultListId ? `List ${defaultListId}` : undefined}
|
|
338
|
+
selectClassName={select}
|
|
339
|
+
onChange={(l) => setListId(l ? String(l.id) : "")}
|
|
340
|
+
/>
|
|
341
|
+
<FirstNotice className="sm:col-span-3" shown={listOf<MailboxOption>(mailboxes.data).length} limit={MAILBOX_LIMIT} noun="mailboxes" />
|
|
335
342
|
</div>
|
|
336
343
|
) : null}
|
|
337
344
|
{need.aiPhone || need.dialerPhone || need.linkedin ? (
|
|
@@ -373,12 +380,11 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
373
380
|
{String(objectOf(linkedin.data).message ?? "No LinkedIn account is connected.")} Connect one in the {brand.linkLabel} app.
|
|
374
381
|
</p>
|
|
375
382
|
) : null}
|
|
376
|
-
{mailboxes.error ||
|
|
383
|
+
{mailboxes.error || phones.error || linkedin.error ? (
|
|
377
384
|
<ApiError
|
|
378
|
-
error={mailboxes.error ??
|
|
385
|
+
error={mailboxes.error ?? phones.error ?? linkedin.error}
|
|
379
386
|
onRetry={() => {
|
|
380
387
|
void mailboxes.refetch();
|
|
381
|
-
void lists.refetch();
|
|
382
388
|
if (need.aiPhone || need.dialerPhone) void phones.refetch();
|
|
383
389
|
if (need.linkedin) void linkedin.refetch();
|
|
384
390
|
}}
|
|
@@ -393,6 +399,7 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
393
399
|
index={i + 1}
|
|
394
400
|
onChange={(next) => setSteps((all) => all.map((x) => (x.key === s.key ? next : x)))}
|
|
395
401
|
onRemove={() => (s.id ? setPending({ kind: "delete", step: s }) : setSteps((all) => all.filter((x) => x.key !== s.key)))}
|
|
402
|
+
renderAssist={(step, change) => <StepAiAssist step={step} onChange={change} sequenceId={sequenceId} stepOrder={i + 1} />}
|
|
396
403
|
/>
|
|
397
404
|
<div className="flex gap-2 text-xs">
|
|
398
405
|
{!s.id ? (
|
|
@@ -422,7 +429,9 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
422
429
|
Add {KIND_LABEL[k].split(" ").map((w) => (/^[A-Z]{2,}$/.test(w) ? w : w.toLowerCase())).join(" ")}
|
|
423
430
|
</Button>
|
|
424
431
|
))}
|
|
432
|
+
<Button size="sm" variant="ghost" onClick={() => setPickingTemplate((p) => !p)}>{pickingTemplate ? "Hide step templates" : "From a step template"}</Button>
|
|
425
433
|
</div>
|
|
434
|
+
{pickingTemplate ? <StepTemplatePicker onPick={(step) => { setSteps((s) => [...s, step]); setPickingTemplate(false); }} /> : null}
|
|
426
435
|
|
|
427
436
|
{editing ? (
|
|
428
437
|
<Button
|
|
@@ -449,7 +458,7 @@ export function SequenceBuilder({ sequenceId, defaultListId, ownerEmail, onSaved
|
|
|
449
458
|
{run.error ? <ApiError error={run.error} onRetry={run.reset} /> : null}
|
|
450
459
|
{append.error ? <ApiError error={append.error} onRetry={append.reset} /> : null}
|
|
451
460
|
{saved ? (
|
|
452
|
-
<p role="status" className="text-sm text-
|
|
461
|
+
<p role="status" className="text-sm text-positive-foreground">
|
|
453
462
|
{saved}
|
|
454
463
|
</p>
|
|
455
464
|
) : null}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequenceCampaignLink: which campaign a sequence belongs to. Shows the linked campaign with an "Open campaign"
|
|
5
|
+
* button, lets you unlink it, or, when there is none, pick a campaign and link it. Use it in
|
|
6
|
+
* `SequencesScreen.renderDetailActions` (the row it passes is the full sequence, so it carries the campaign id):
|
|
7
|
+
* renderDetailActions={(s) => <SequenceCampaignLink sequence={s} onOpenCampaign={(id) => router.push(`/campaigns?id=${id}`)} />}
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. getCampaign GET /campaigns/{id} (read; the linked campaign's name)
|
|
11
|
+
* listCampaignsForPicker GET /campaigns/picker (read; the choices, only while the sequence has no campaign)
|
|
12
|
+
* 2. linkSequenceCampaign PUT /sequences/{id}/campaign (write)
|
|
13
|
+
* 3. unlinkSequenceCampaign DELETE /sequences/{id}/campaign (DESTRUCTIVE tier -> ConfirmSpendDialog; the campaign is kept)
|
|
14
|
+
* Linking never replaces silently: while a campaign is linked, the picker is hidden, so you unlink first.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ExternalLink, Link2, Unlink } from "lucide-react";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { g8Delete, g8Get, g8Put } from "@/g8/lib/g8-client";
|
|
25
|
+
import { listOf, objectOf } from "../lib/sequencer-utils";
|
|
26
|
+
|
|
27
|
+
export interface SequenceCampaignLinkProps {
|
|
28
|
+
/** The full sequence (from `renderDetailActions`): its id and `campaign_builder_campaign_id`. */
|
|
29
|
+
sequence: { id: string; name?: string | null; campaign_builder_campaign_id?: string | null };
|
|
30
|
+
/** Route to the campaign in your app. Without it the campaign's name is shown without a button. */
|
|
31
|
+
onOpenCampaign?: (campaignId: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Choice { id: string; name?: string | null }
|
|
35
|
+
|
|
36
|
+
export function SequenceCampaignLink({ sequence, onOpenCampaign }: SequenceCampaignLinkProps) {
|
|
37
|
+
const linked = sequence.campaign_builder_campaign_id ? String(sequence.campaign_builder_campaign_id) : "";
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
const [choice, setChoice] = useState("");
|
|
40
|
+
const [confirm, setConfirm] = useState(false);
|
|
41
|
+
const campaign = useG8Query((signal) => g8Get(`/campaigns/${encodeURIComponent(linked)}`, undefined, { signal }), [linked], { enabled: Boolean(linked), key: ["campaigns", "sequence-link", linked] });
|
|
42
|
+
const choices = useG8Query((signal) => g8Get("/campaigns/picker", undefined, { signal }), [], { enabled: open && !linked, key: ["sequences", "campaign-choices"] });
|
|
43
|
+
const refresh = [["sequences", "detail", sequence.id], ["sequences", "list"]] as const;
|
|
44
|
+
const link = useG8Mutation((id: string) => g8Put(`/sequences/${encodeURIComponent(sequence.id)}/campaign`, { campaign_id: id }), { invalidates: refresh, onSuccess: () => { setOpen(false); setChoice(""); } });
|
|
45
|
+
const unlink = useG8Mutation(async () => {
|
|
46
|
+
try { await g8Delete(`/sequences/${encodeURIComponent(sequence.id)}/campaign`); } catch (err) { if ((err as { status?: number }).status !== 404) throw err; }
|
|
47
|
+
}, { invalidates: refresh, onSuccess: () => setConfirm(false), onError: () => setConfirm(false) });
|
|
48
|
+
|
|
49
|
+
const name = (objectOf(campaign.data).name as string | undefined) || `Campaign ${linked}`;
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
{linked ? (
|
|
53
|
+
<>
|
|
54
|
+
{campaign.loading && !campaign.data ? <Skeleton className="h-8 w-28" aria-label="Loading campaign" /> : onOpenCampaign ? (
|
|
55
|
+
<Button size="sm" variant="outline" onClick={() => onOpenCampaign(linked)} aria-label={`Open campaign ${name}`}><ExternalLink className="mr-1 h-4 w-4" aria-hidden />Open campaign</Button>
|
|
56
|
+
) : <span className="text-sm text-muted-foreground">Campaign: {name}</span>}
|
|
57
|
+
<Button size="sm" variant="ghost" onClick={() => setConfirm(true)}><Unlink className="mr-1 h-4 w-4" aria-hidden />Unlink campaign</Button>
|
|
58
|
+
</>
|
|
59
|
+
) : open ? (
|
|
60
|
+
<span className="flex items-center gap-2">
|
|
61
|
+
{choices.error ? <ApiError error={choices.error} title="Could not load campaigns" onRetry={() => void choices.refetch()} /> : (
|
|
62
|
+
<select aria-label="Campaign to link" className="h-8 rounded-md border bg-background px-2 text-sm" value={choice} onChange={(e) => setChoice(e.target.value)}>
|
|
63
|
+
<option value="">{choices.loading && !choices.data ? "Loading campaigns" : listOf<Choice>(choices.data).length ? "Choose a campaign" : "No campaigns yet"}</option>
|
|
64
|
+
{listOf<Choice>(choices.data).map((c) => <option key={c.id} value={c.id}>{c.name ?? c.id}</option>)}
|
|
65
|
+
</select>
|
|
66
|
+
)}
|
|
67
|
+
<Button size="sm" disabled={!choice} loading={link.loading} onClick={() => void link.mutate(choice)}>Link</Button>
|
|
68
|
+
<Button size="sm" variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
|
|
69
|
+
</span>
|
|
70
|
+
) : (
|
|
71
|
+
<Button size="sm" variant="outline" onClick={() => setOpen(true)}><Link2 className="mr-1 h-4 w-4" aria-hidden />Link to campaign</Button>
|
|
72
|
+
)}
|
|
73
|
+
{link.error ? <ApiError error={link.error} title="Could not link the campaign" onRetry={link.reset} /> : null}
|
|
74
|
+
{unlink.error ? <ApiError error={unlink.error} title="Could not unlink the campaign" onRetry={unlink.reset} /> : null}
|
|
75
|
+
<ConfirmSpendDialog open={confirm} tier="destructive" title={`Unlink "${sequence.name ?? "this sequence"}" from ${name}?`} description="The campaign and the sequence are both kept. Only the tie between them is removed." confirmLabel="Unlink" affected={1} confirming={unlink.loading} onCancel={() => setConfirm(false)} onConfirm={() => void unlink.mutate(undefined)} />
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* 5. archiveSequence POST /sequences/{id}/archive (destructive: confirmed)
|
|
18
18
|
* Enroll contacts: EnrollSheet (ListPicker + ContactTable, or the whole list) then EnrollDialog (external: confirmed).
|
|
19
19
|
* Steps, Contacts and Results tabs compose SequenceStepsPanel, SequenceEnrollments and SequenceStats.
|
|
20
|
+
* Manage (SequenceManageActions): edit and rename (external, confirmed), sync the audience (external, confirmed), save as
|
|
21
|
+
* template, delete (destructive, confirmed). The Senders tab (SequenceSendersPanel) lists the senders and removes one (destructive, confirmed).
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
import { useState } from "react";
|
|
@@ -39,6 +41,8 @@ import { fieldItems, launchCheck, listOf, objectOf, statusGroup } from "../lib/s
|
|
|
39
41
|
import type { SequenceRow } from "../lib/sequencer-utils";
|
|
40
42
|
import { EnrollDialog } from "./enroll-dialog";
|
|
41
43
|
import { SequenceEnrollments } from "./sequence-enrollments";
|
|
44
|
+
import { SequenceManageActions } from "./sequence-manage-actions";
|
|
45
|
+
import { SequenceSendersPanel } from "./sequence-senders-panel";
|
|
42
46
|
import { SequenceStats } from "./sequence-stats";
|
|
43
47
|
import { SequenceStepsPanel } from "./sequence-steps-panel";
|
|
44
48
|
import { pluralWord } from "@/g8/lib/plural";
|
|
@@ -143,6 +147,8 @@ export interface SequenceDetailProps {
|
|
|
143
147
|
refreshKey?: number;
|
|
144
148
|
/** Open another sequence (the duplicate). */
|
|
145
149
|
onOpenSequence?: (sequenceId: string) => void;
|
|
150
|
+
/** Open an enrolled person in your contacts screen. */
|
|
151
|
+
onOpenContact?: (contactId: number) => void;
|
|
146
152
|
renderActions?: (sequence: SequenceRow) => ReactNode;
|
|
147
153
|
extraTabs?: { id: string; label: string; render: (sequence: SequenceRow) => ReactNode }[];
|
|
148
154
|
className?: string;
|
|
@@ -201,6 +207,7 @@ export function SequenceDetail(props: SequenceDetailProps) {
|
|
|
201
207
|
<Button size="sm" variant="outline" onClick={() => setEnrolling(true)}><UserPlus className="mr-1 h-4 w-4" aria-hidden /> Enroll contacts</Button>
|
|
202
208
|
<Button size="sm" variant="ghost" aria-label="Duplicate sequence" loading={duplicate.loading} onClick={() => void duplicate.mutate(undefined)}><Copy className="h-4 w-4" aria-hidden /></Button>
|
|
203
209
|
{group !== "ended" ? <Button size="sm" variant="ghost" onClick={() => void prepare.mutate("archive")}>Archive sequence</Button> : null}
|
|
210
|
+
<SequenceManageActions sequence={row} onChanged={changed} onDeleted={() => onClose?.()} />
|
|
204
211
|
{props.renderActions?.(row)}
|
|
205
212
|
{enrolling ? <EnrollSheet sequence={row} onClose={() => setEnrolling(false)} onEnrolled={() => { setEnrolling(false); setStatsKey((k) => k + 1); changed(); }} /> : null}
|
|
206
213
|
{p ? (
|
|
@@ -211,7 +218,7 @@ export function SequenceDetail(props: SequenceDetailProps) {
|
|
|
211
218
|
description={
|
|
212
219
|
<>
|
|
213
220
|
<p>{COPY[p.action].description}</p>
|
|
214
|
-
{p.warnings.length > 0 ? <ul className="mt-2 list-disc pl-5 text-
|
|
221
|
+
{p.warnings.length > 0 ? <ul className="mt-2 list-disc pl-5 text-warning-foreground">{p.warnings.map((w) => <li key={w}>{w}</li>)}</ul> : null}
|
|
215
222
|
</>
|
|
216
223
|
}
|
|
217
224
|
recipients={p.action === "archive" ? undefined : (row.contact_count ?? undefined)}
|
|
@@ -250,7 +257,8 @@ export function SequenceDetail(props: SequenceDetailProps) {
|
|
|
250
257
|
),
|
|
251
258
|
},
|
|
252
259
|
{ id: "steps", label: "Steps", count: row.step_count ?? undefined, content: <SequenceStepsPanel sequenceId={id} stepId={props.stepId} onStepChange={props.onStepChange} onChanged={changed} /> },
|
|
253
|
-
{ id: "contacts", label: "Contacts", count: row.contact_count ?? undefined, content: <SequenceEnrollments key={statsKey} sequenceId={id} contactId={props.contactId} onContactChange={props.onContactChange} /> },
|
|
260
|
+
{ id: "contacts", label: "Contacts", count: row.contact_count ?? undefined, content: <SequenceEnrollments key={statsKey} sequenceId={id} contactId={props.contactId} onContactChange={props.onContactChange} onOpenContact={props.onOpenContact} /> },
|
|
261
|
+
{ id: "senders", label: "Senders", content: <SequenceSendersPanel sequenceId={id} onChanged={changed} /> },
|
|
254
262
|
{ id: "results", label: "Results", content: <SequenceStats key={`${id}:${statsKey}`} sequenceId={id} refreshSeconds={group === "live" ? 60 : undefined} /> },
|
|
255
263
|
...(props.extraTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(row) })),
|
|
256
264
|
]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequenceEditForm: rename a sequence and change its own settings (description, stop on reply, same email thread, pick
|
|
5
|
+
* up new contacts).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getSequence GET /sequences/{id} (read) the record the form is seeded from
|
|
9
|
+
* 2. updateSequence PATCH /sequences/{id} (EXTERNAL: tier `external`, because changing a live sequence changes what
|
|
10
|
+
* real people receive) -> ConfirmSpendDialog first, naming each change and the contacts in flight
|
|
11
|
+
*
|
|
12
|
+
* The form never saves over a record it has not loaded: nothing renders until the sequence has loaded, Save stays
|
|
13
|
+
* disabled until something differs from what was loaded, and the request carries ONLY the changed fields (a partial
|
|
14
|
+
* PATCH, so a field this form does not show is never reset). A 409 (the sequence is mid-transition: launching, pausing)
|
|
15
|
+
* is shown as the API words it. The audience list, mailboxes and steps are not edited here: the API cannot change the
|
|
16
|
+
* list after creation, senders are SequenceSendersPanel, steps are SequenceStepsPanel.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { g8Get, g8Patch } from "@/g8/lib/g8-client";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { changeSummary, editableSequence, sequenceChanges } from "../lib/sequencer-manage";
|
|
26
|
+
import type { EditableSequence } from "../lib/sequencer-manage";
|
|
27
|
+
import { objectOf, statusGroup } from "../lib/sequencer-utils";
|
|
28
|
+
|
|
29
|
+
export interface SequenceEditFormProps {
|
|
30
|
+
sequenceId: string;
|
|
31
|
+
/** Called after the change was saved. */
|
|
32
|
+
onSaved?: () => void;
|
|
33
|
+
onCancel?: () => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Fields({ loaded, contacts, live, sequenceId, onSaved, onCancel }: { loaded: EditableSequence; contacts: number; live: boolean; sequenceId: string; onSaved?: () => void; onCancel?: () => void }) {
|
|
38
|
+
const [draft, setDraft] = useState<EditableSequence>(loaded);
|
|
39
|
+
const [confirming, setConfirming] = useState(false);
|
|
40
|
+
const changes = sequenceChanges(loaded, draft);
|
|
41
|
+
const dirty = Object.keys(changes).length > 0;
|
|
42
|
+
const save = useG8Mutation(async () => {
|
|
43
|
+
await g8Patch(`/sequences/${encodeURIComponent(sequenceId)}`, changes);
|
|
44
|
+
}, { invalidates: ["sequences"] });
|
|
45
|
+
const set = <K extends keyof EditableSequence>(key: K, value: EditableSequence[K]) => setDraft((d) => ({ ...d, [key]: value }));
|
|
46
|
+
const confirm = async () => {
|
|
47
|
+
try {
|
|
48
|
+
await save.mutateAsync(undefined);
|
|
49
|
+
setConfirming(false);
|
|
50
|
+
onSaved?.();
|
|
51
|
+
} catch {
|
|
52
|
+
setConfirming(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const check = (key: "finish_on_reply" | "send_in_same_thread" | "wait_for_new_contacts", label: string) => (
|
|
56
|
+
<label className="flex items-center gap-2 text-sm">
|
|
57
|
+
<input type="checkbox" checked={draft[key]} onChange={(e) => set(key, e.target.checked)} />
|
|
58
|
+
{label}
|
|
59
|
+
</label>
|
|
60
|
+
);
|
|
61
|
+
return (
|
|
62
|
+
<form className="space-y-3" onSubmit={(e) => { e.preventDefault(); if (dirty && draft.name.trim()) setConfirming(true); }}>
|
|
63
|
+
<label className="block space-y-1 text-sm"><span className="font-medium">Name</span><Input aria-label="Sequence name" value={draft.name} onChange={(e) => set("name", e.target.value)} /></label>
|
|
64
|
+
<label className="block space-y-1 text-sm"><span className="font-medium">Description</span><Textarea aria-label="Description" rows={3} value={draft.description} onChange={(e) => set("description", e.target.value)} /></label>
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
{check("finish_on_reply", "Stop sending to a contact once they reply")}
|
|
67
|
+
{check("send_in_same_thread", "Send follow-up emails in the same thread")}
|
|
68
|
+
{check("wait_for_new_contacts", "Keep picking up contacts added to the audience list")}
|
|
69
|
+
</div>
|
|
70
|
+
{live ? <p role="note" className="text-xs text-warning-foreground">This sequence is live. Changes apply to contacts already in it.</p> : null}
|
|
71
|
+
{save.error ? <ApiError error={save.error} title="Could not save the sequence" /> : null}
|
|
72
|
+
<div className="flex justify-end gap-2">
|
|
73
|
+
{onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
|
|
74
|
+
<Button type="submit" disabled={!dirty || !draft.name.trim() || save.loading}>Save changes</Button>
|
|
75
|
+
</div>
|
|
76
|
+
<ConfirmSpendDialog
|
|
77
|
+
open={confirming}
|
|
78
|
+
tier="external"
|
|
79
|
+
title={`Save changes to "${loaded.name}"?`}
|
|
80
|
+
description={<ul className="list-disc pl-5">{changeSummary(changes).map((c) => <li key={c}>{c}</li>)}</ul>}
|
|
81
|
+
recipients={contacts > 0 ? contacts : undefined}
|
|
82
|
+
confirmLabel="Save changes"
|
|
83
|
+
confirming={save.loading}
|
|
84
|
+
onConfirm={() => void confirm()}
|
|
85
|
+
onCancel={() => setConfirming(false)}
|
|
86
|
+
/>
|
|
87
|
+
</form>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function SequenceEditForm({ sequenceId, onSaved, onCancel, className }: SequenceEditFormProps) {
|
|
92
|
+
const [version, setVersion] = useState(0);
|
|
93
|
+
// No refetch on window focus: it would reseed the form and wipe what the person is typing.
|
|
94
|
+
const seq = useG8Query((signal) => g8Get(`/sequences/${encodeURIComponent(sequenceId)}`, undefined, { signal }), [sequenceId], { key: ["sequences", "edit", sequenceId], keepPrevious: false, refetchOnFocus: false });
|
|
95
|
+
if (seq.error) return <ApiError error={seq.error} title="Could not load the sequence" onRetry={() => void seq.refetch()} className={className} />;
|
|
96
|
+
if (!seq.data || seq.isPlaceholderData) return <Skeleton aria-busy="true" className={`h-48 w-full ${className ?? ""}`} />;
|
|
97
|
+
const record = objectOf(seq.data);
|
|
98
|
+
return (
|
|
99
|
+
<div className={className}>
|
|
100
|
+
<Fields
|
|
101
|
+
key={`${sequenceId}:${version}`}
|
|
102
|
+
loaded={editableSequence(record)}
|
|
103
|
+
contacts={Number(record.contact_count ?? 0) || 0}
|
|
104
|
+
live={statusGroup(typeof record.status === "string" ? record.status : null) === "live"}
|
|
105
|
+
sequenceId={sequenceId}
|
|
106
|
+
onSaved={() => { void seq.refetch().then(() => setVersion((v) => v + 1)); onSaved?.(); }}
|
|
107
|
+
onCancel={onCancel}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -24,6 +24,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
24
24
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
25
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
26
26
|
import { ContactDetail } from "@/g8/crm/components/contact-detail";
|
|
27
|
+
import { WithdrawContactButton } from "./enrollment-withdraw";
|
|
27
28
|
import { fieldItems, listOf } from "../lib/sequencer-utils";
|
|
28
29
|
|
|
29
30
|
export interface SequenceEnrollmentsProps {
|
|
@@ -31,6 +32,8 @@ export interface SequenceEnrollmentsProps {
|
|
|
31
32
|
/** The open enrollment, by contact id. Controlled so a screen can deep-link it. */
|
|
32
33
|
contactId?: string;
|
|
33
34
|
onContactChange?: (contactId: string | null) => void;
|
|
35
|
+
/** Open the enrolled person in your contacts screen (adds an "Open in contacts" button). */
|
|
36
|
+
onOpenContact?: (contactId: number) => void;
|
|
34
37
|
pageSize?: number;
|
|
35
38
|
className?: string;
|
|
36
39
|
}
|
|
@@ -41,7 +44,7 @@ const STATES = ["", "waiting", "paused", "completed", "replied", "bounced", "ski
|
|
|
41
44
|
const TONE: Record<string, Tone> = { waiting: "info", paused: "warning", completed: "positive", replied: "positive", bounced: "negative", skipped: "neutral" };
|
|
42
45
|
const nameOf = (e: Enrollment) => e.name || e.email || (e.contact_id != null ? `Contact ${e.contact_id}` : "Contact");
|
|
43
46
|
|
|
44
|
-
export function SequenceEnrollments({ sequenceId, contactId, onContactChange, pageSize = 25, className }: SequenceEnrollmentsProps) {
|
|
47
|
+
export function SequenceEnrollments({ sequenceId, contactId, onContactChange, onOpenContact, pageSize = 25, className }: SequenceEnrollmentsProps) {
|
|
45
48
|
const [state, setState] = useState("");
|
|
46
49
|
const [page, setPage] = useState(1);
|
|
47
50
|
const [resume, setResume] = useState<Enrollment | null>(null);
|
|
@@ -89,7 +92,7 @@ export function SequenceEnrollments({ sequenceId, contactId, onContactChange, pa
|
|
|
89
92
|
<Button size="sm" variant="outline" disabled={rows.length < pageSize} onClick={() => setPage((p) => p + 1)}>Next</Button>
|
|
90
93
|
</div>
|
|
91
94
|
{open ? (
|
|
92
|
-
<SectionCard title={`${nameOf(open)} in this sequence`} actions={<>{actions(open)}<Button size="sm" variant="ghost" onClick={() => onContactChange?.(null)}>Close</Button></>}>
|
|
95
|
+
<SectionCard title={`${nameOf(open)} in this sequence`} actions={<>{actions(open)}{onOpenContact && open.contact_id != null ? <Button size="sm" variant="outline" onClick={() => onOpenContact(open.contact_id as number)}>Open in contacts</Button> : null}{open.contact_id != null ? <WithdrawContactButton sequenceId={sequenceId} contactId={open.contact_id} contactName={nameOf(open)} onWithdrawn={() => { onContactChange?.(null); void list.refetch(); }} /> : null}<Button size="sm" variant="ghost" onClick={() => onContactChange?.(null)}>Close</Button></>}>
|
|
93
96
|
<KeyValueList columns={2} items={fieldItems(open as Record<string, unknown>)} />
|
|
94
97
|
{open.contact_id != null ? (
|
|
95
98
|
<SectionCard title="Contact" description="The full contact record.">
|
|
@@ -204,7 +204,7 @@ export function SequenceList({ status, pageSize = 25, onOpen, onCreate, onChange
|
|
|
204
204
|
<>
|
|
205
205
|
<p>{COPY[pending.action].description}</p>
|
|
206
206
|
{pending.warnings.length > 0 ? (
|
|
207
|
-
<ul className="mt-2 list-disc pl-5 text-
|
|
207
|
+
<ul className="mt-2 list-disc pl-5 text-warning-foreground">
|
|
208
208
|
{pending.warnings.map((w) => (
|
|
209
209
|
<li key={w}>{w}</li>
|
|
210
210
|
))}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequenceManageActions: the buttons that manage a sequence itself, for a sequence detail's action row: Edit (rename and
|
|
5
|
+
* settings), Sync audience, Save as template, Delete.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* Edit SequenceEditForm in a sheet: getSequence (read) then updateSequence PATCH /sequences/{id} (EXTERNAL, confirmed there)
|
|
9
|
+
* Sync audience POST /sequences/{id}/sync (sequences.syncSequence) EXTERNAL -> ConfirmSpendDialog first.
|
|
10
|
+
* Re-pulls the audience list so contacts added to it since launch are ENROLLED (real recipients).
|
|
11
|
+
* Save as template GET /sequences/{id}/steps (sequenceLifecycle.listSequenceSteps) read
|
|
12
|
+
* POST /sequencer/templates/sequences (sequences.createSequenceTemplate) write; the steps are copied, not linked
|
|
13
|
+
* Delete DELETE /sequences/{id} (sequenceLifecycle.deleteSequence) DESTRUCTIVE -> ConfirmSpendDialog first.
|
|
14
|
+
* The API's DELETE is a SOFT delete, the same as archive: the sequence stops, leaves every list, and cannot be
|
|
15
|
+
* restored through the API. The Archive button in SequenceDetail stays for the same effect.
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { BookmarkPlus, Pencil, RefreshCw, Trash2 } from "lucide-react";
|
|
19
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
23
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { templateBodyFromSteps } from "../lib/sequencer-manage";
|
|
26
|
+
import { listOf, statusGroup } from "../lib/sequencer-utils";
|
|
27
|
+
import type { ApiStep, SequenceRow } from "../lib/sequencer-utils";
|
|
28
|
+
import { SequenceEditForm } from "./sequence-edit-form";
|
|
29
|
+
|
|
30
|
+
export interface SequenceManageActionsProps {
|
|
31
|
+
sequence: SequenceRow;
|
|
32
|
+
/** Called after any change so the list and detail can refresh. */
|
|
33
|
+
onChanged?: () => void;
|
|
34
|
+
/** Called after the sequence was deleted (close its detail here). */
|
|
35
|
+
onDeleted?: () => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Dialog = "sync" | "delete" | "template" | null;
|
|
40
|
+
|
|
41
|
+
export function SequenceManageActions({ sequence, onChanged, onDeleted, className }: SequenceManageActionsProps) {
|
|
42
|
+
const [dialog, setDialog] = useState<Dialog>(null);
|
|
43
|
+
const [editing, setEditing] = useState(false);
|
|
44
|
+
const [templateName, setTemplateName] = useState("");
|
|
45
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
46
|
+
const name = sequence.name ?? `Sequence ${sequence.id}`;
|
|
47
|
+
const group = statusGroup(sequence.status);
|
|
48
|
+
const id = encodeURIComponent(sequence.id);
|
|
49
|
+
|
|
50
|
+
const sync = useG8Mutation(async () => {
|
|
51
|
+
await g8Post(`/sequences/${id}/sync`);
|
|
52
|
+
setDialog(null);
|
|
53
|
+
setNotice("The audience list was synced.");
|
|
54
|
+
onChanged?.();
|
|
55
|
+
}, { invalidates: ["sequences"], onError: () => setDialog(null) });
|
|
56
|
+
const remove = useG8Mutation(async () => {
|
|
57
|
+
await g8Delete(`/sequences/${id}`);
|
|
58
|
+
setDialog(null);
|
|
59
|
+
onChanged?.();
|
|
60
|
+
onDeleted?.();
|
|
61
|
+
}, { invalidates: ["sequences"], onError: () => setDialog(null) });
|
|
62
|
+
const saveTemplate = useG8Mutation(async () => {
|
|
63
|
+
const steps = listOf<ApiStep>(await g8Get(`/sequences/${id}/steps`));
|
|
64
|
+
if (steps.length === 0) throw new Error("This sequence has no steps to save.");
|
|
65
|
+
await g8Post("/sequencer/templates/sequences", templateBodyFromSteps(templateName, "", steps));
|
|
66
|
+
setDialog(null);
|
|
67
|
+
setNotice(`Saved "${templateName.trim()}" as a template.`);
|
|
68
|
+
}, { onError: () => undefined });
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<span className={`contents ${className ?? ""}`}>
|
|
73
|
+
<Button size="sm" variant="outline" onClick={() => setEditing(true)}><Pencil className="mr-1 h-4 w-4" aria-hidden />Edit</Button>
|
|
74
|
+
{group === "live" || group === "paused" ? <Button size="sm" variant="outline" onClick={() => setDialog("sync")}><RefreshCw className="mr-1 h-4 w-4" aria-hidden />Sync audience</Button> : null}
|
|
75
|
+
<Button size="sm" variant="ghost" onClick={() => { setTemplateName(`${name} template`); setDialog("template"); }}><BookmarkPlus className="mr-1 h-4 w-4" aria-hidden />Save as template</Button>
|
|
76
|
+
<Button size="sm" variant="ghost" onClick={() => setDialog("delete")}><Trash2 className="mr-1 h-4 w-4" aria-hidden />Delete sequence</Button>
|
|
77
|
+
</span>
|
|
78
|
+
{notice ? <p role="status" className="basis-full text-xs text-muted-foreground">{notice}</p> : null}
|
|
79
|
+
{[sync.error, remove.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Could not update the sequence" /> : null))}
|
|
80
|
+
|
|
81
|
+
<FormSheet open={editing} onOpenChange={setEditing} title={`Edit ${name}`} description="Rename it or change how it behaves. Only what you change is saved.">
|
|
82
|
+
{editing ? <SequenceEditForm sequenceId={sequence.id} onCancel={() => setEditing(false)} onSaved={() => { setEditing(false); onChanged?.(); }} /> : null}
|
|
83
|
+
</FormSheet>
|
|
84
|
+
|
|
85
|
+
<FormSheet
|
|
86
|
+
open={dialog === "template"}
|
|
87
|
+
onOpenChange={(o) => !o && setDialog(null)}
|
|
88
|
+
title="Save as template"
|
|
89
|
+
description="Copies the steps so you can start new sequences from them. The template does not change when this sequence does."
|
|
90
|
+
footer={<><Button variant="outline" onClick={() => setDialog(null)}>Cancel</Button><Button loading={saveTemplate.loading} disabled={!templateName.trim()} onClick={() => void saveTemplate.mutate(undefined)}>Save template</Button></>}
|
|
91
|
+
>
|
|
92
|
+
<Input aria-label="Template name" value={templateName} onChange={(e) => setTemplateName(e.target.value)} />
|
|
93
|
+
{saveTemplate.error ? <ApiError error={saveTemplate.error} title="Could not save the template" /> : null}
|
|
94
|
+
</FormSheet>
|
|
95
|
+
|
|
96
|
+
<ConfirmSpendDialog
|
|
97
|
+
open={dialog === "sync"}
|
|
98
|
+
tier="external"
|
|
99
|
+
title={`Sync the audience of "${name}"?`}
|
|
100
|
+
description="Contacts added to the audience list since the last sync are enrolled now and start receiving the steps."
|
|
101
|
+
confirmLabel="Sync audience"
|
|
102
|
+
confirming={sync.loading}
|
|
103
|
+
onConfirm={() => void sync.mutate(undefined)}
|
|
104
|
+
onCancel={() => setDialog(null)}
|
|
105
|
+
/>
|
|
106
|
+
<ConfirmSpendDialog
|
|
107
|
+
open={dialog === "delete"}
|
|
108
|
+
tier="destructive"
|
|
109
|
+
title={`Delete "${name}"?`}
|
|
110
|
+
description="The sequence stops sending and disappears from your lists. The API cannot restore a deleted sequence. Contacts already messaged keep what they received."
|
|
111
|
+
confirmLabel="Delete sequence"
|
|
112
|
+
affected={1}
|
|
113
|
+
confirming={remove.loading}
|
|
114
|
+
onConfirm={() => void remove.mutate(undefined)}
|
|
115
|
+
onCancel={() => setDialog(null)}
|
|
116
|
+
/>
|
|
117
|
+
</>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SequenceSendersPanel: the senders a sequence sends from (mailboxes, phone numbers, LinkedIn accounts) and a way to
|
|
5
|
+
* take one off.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listSequenceChannels GET /sequences/{id}/channels (read)
|
|
9
|
+
* 2. removeSequenceChannel DELETE /sequences/{id}/channels/{sequence_channel_id} (DESTRUCTIVE) -> ConfirmSpendDialog first
|
|
10
|
+
* Removing the only sender of a step type means those steps can no longer send and the sequence cannot launch (or keeps
|
|
11
|
+
* a live step stuck); the dialog says which step types lose their sender. Adding senders to an existing sequence is
|
|
12
|
+
* done where its steps are added (SequenceBuilder in edit mode), which asks for the sender each new step type needs.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Trash2 } from "lucide-react";
|
|
16
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
20
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
21
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { listOf } from "../lib/sequencer-utils";
|
|
25
|
+
|
|
26
|
+
export interface SequenceSendersPanelProps {
|
|
27
|
+
sequenceId: string;
|
|
28
|
+
/** Called after a sender was removed so a detail can recheck whether the sequence can launch. */
|
|
29
|
+
onChanged?: () => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Channel { id?: string | number; channel_id?: number; channel_type?: string | null; channel_value?: string | null }
|
|
34
|
+
|
|
35
|
+
const LABEL: Record<string, string> = { GMAIL: "Gmail mailbox", INBOXKIT: "Managed mailbox", SMTP: "SMTP mailbox", PHONE: "Phone number", LINKEDIN: "LinkedIn account", LINKEDIN_NETRION: "LinkedIn account", SMS: "SMS number", WHATSAPP: "WhatsApp number" };
|
|
36
|
+
const STEPS_LOST: Record<string, string> = { GMAIL: "email", INBOXKIT: "email", SMTP: "email", PHONE: "call", LINKEDIN: "LinkedIn", LINKEDIN_NETRION: "LinkedIn", SMS: "SMS", WHATSAPP: "WhatsApp" };
|
|
37
|
+
const typeOf = (c: Channel) => (c.channel_type ?? "").toUpperCase();
|
|
38
|
+
|
|
39
|
+
export function SequenceSendersPanel({ sequenceId, onChanged, className }: SequenceSendersPanelProps) {
|
|
40
|
+
const [removing, setRemoving] = useState<Channel | null>(null);
|
|
41
|
+
const list = useG8Query((signal) => g8Get(`/sequences/${encodeURIComponent(sequenceId)}/channels`, undefined, { signal }), [sequenceId], { key: ["sequences", "channels", sequenceId] });
|
|
42
|
+
const rows = listOf<Channel>(list.data);
|
|
43
|
+
const remove = useG8Mutation(async (c: Channel) => {
|
|
44
|
+
await g8Delete(`/sequences/${encodeURIComponent(sequenceId)}/channels/${encodeURIComponent(String(c.id))}`);
|
|
45
|
+
setRemoving(null);
|
|
46
|
+
await list.refetch();
|
|
47
|
+
onChanged?.();
|
|
48
|
+
}, { invalidates: ["sequences"], onError: () => setRemoving(null) });
|
|
49
|
+
|
|
50
|
+
if (list.error) return <ApiError error={list.error} title="Could not load the senders" onRetry={() => void list.refetch()} className={className} />;
|
|
51
|
+
if (list.loading && !list.data) return <Skeleton aria-busy="true" className={`h-24 w-full ${className ?? ""}`} />;
|
|
52
|
+
const others = removing ? rows.filter((c) => c.id !== removing.id && STEPS_LOST[typeOf(c)] === STEPS_LOST[typeOf(removing)]).length : 0;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={`space-y-3 ${className ?? ""}`}>
|
|
56
|
+
<h3 className="font-medium">Senders</h3>
|
|
57
|
+
{rows.length === 0 ? (
|
|
58
|
+
<EmptyState compact title="No sender yet" description="Steps need a sender to go out: a mailbox for emails, a number for calls, an account for LinkedIn. Add steps with their sender in the Steps tab." />
|
|
59
|
+
) : (
|
|
60
|
+
<ul aria-label="Senders" className="divide-y rounded-md border border-border text-sm">
|
|
61
|
+
{rows.map((c) => (
|
|
62
|
+
<li key={String(c.id ?? `${c.channel_type}:${c.channel_id}`)} className="flex items-center gap-3 p-2">
|
|
63
|
+
<StatusPill tone="neutral">{LABEL[typeOf(c)] ?? (c.channel_type || "Sender")}</StatusPill>
|
|
64
|
+
<span className="min-w-0 flex-1 truncate">{c.channel_value || `#${c.channel_id ?? ""}`}</span>
|
|
65
|
+
<Button size="sm" variant="ghost" aria-label={`Remove sender ${c.channel_value ?? c.channel_id ?? ""}`} disabled={c.id == null} onClick={() => setRemoving(c)}><Trash2 className="size-4" aria-hidden /></Button>
|
|
66
|
+
</li>
|
|
67
|
+
))}
|
|
68
|
+
</ul>
|
|
69
|
+
)}
|
|
70
|
+
{remove.error ? <ApiError error={remove.error} title="Could not remove the sender" /> : null}
|
|
71
|
+
<ConfirmSpendDialog
|
|
72
|
+
open={removing != null}
|
|
73
|
+
tier="destructive"
|
|
74
|
+
title={`Remove ${removing?.channel_value ?? "this sender"} from the sequence?`}
|
|
75
|
+
description={others === 0 && removing ? `It is the only ${STEPS_LOST[typeOf(removing)] ?? ""} sender, so those steps can no longer send until you add another.` : "Other senders of the same kind keep the sequence sending."}
|
|
76
|
+
confirmLabel="Remove sender"
|
|
77
|
+
affected={1}
|
|
78
|
+
confirming={remove.loading}
|
|
79
|
+
onConfirm={() => removing && void remove.mutate(removing)}
|
|
80
|
+
onCancel={() => setRemoving(null)}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|