@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
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* 4. reinstateSuppressions POST /contacts/suppressions/reinstate { contact_ids, reason } (write, but it lets
|
|
12
12
|
* sequences and campaigns contact people again, so it goes through ConfirmSpendDialog as "external")
|
|
13
13
|
*
|
|
14
|
+
* No ids to type: contacts to suppress and the contact to check are chosen with a ContactPicker (search by name or work
|
|
15
|
+
* email; GET /contacts, read). Pasting numeric ids still works under "Paste ids instead" / "Check by id instead".
|
|
14
16
|
* The ledger holds ids, not names or emails (no personal data): resolve a name with ContactDetail.
|
|
15
17
|
* Suppressing only ever REDUCES outreach, so it runs without a confirmation.
|
|
16
18
|
*/
|
|
@@ -20,6 +22,8 @@ import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead,
|
|
|
20
22
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
26
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
23
27
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
28
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
29
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
@@ -59,6 +63,8 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
59
63
|
const [category, setCategory] = useState("");
|
|
60
64
|
const [offset, setOffset] = useState(0);
|
|
61
65
|
const [idsText, setIdsText] = useState("");
|
|
66
|
+
const [chosen, setChosen] = useState<ContactOption[]>([]);
|
|
67
|
+
const [checkChosen, setCheckChosen] = useState<ContactOption | null>(null);
|
|
62
68
|
const [addChannel, setAddChannel] = useState("all");
|
|
63
69
|
const [reason, setReason] = useState("");
|
|
64
70
|
const [picked, setPicked] = useState<Set<number>>(new Set());
|
|
@@ -68,7 +74,7 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
68
74
|
|
|
69
75
|
const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
|
|
70
76
|
const add = useG8Mutation((ids: number[]) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: ids, channel: addChannel, reason: reason.trim() || undefined }), {
|
|
71
|
-
onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
|
|
77
|
+
onSuccess: () => { setIdsText(""); setChosen([]); setReason(""); void ledger.refetch(); },
|
|
72
78
|
});
|
|
73
79
|
const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
|
|
74
80
|
onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); setPicked(new Set()); void ledger.refetch(); },
|
|
@@ -76,15 +82,23 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
76
82
|
const check = useG8Mutation((id: number) => g8Get<ContactStatus>(`/contacts/${id}/suppression`));
|
|
77
83
|
|
|
78
84
|
const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
|
|
79
|
-
const ids = parseIds(idsText);
|
|
85
|
+
const ids = [...new Set([...chosen.map((c) => c.id), ...parseIds(idsText)])];
|
|
86
|
+
const checkTarget = checkChosen?.id ?? Number(checkId);
|
|
80
87
|
const selectedContacts = [...picked];
|
|
81
88
|
const togglePick = (contactId: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(contactId)) n.add(contactId); return n; });
|
|
82
89
|
|
|
83
90
|
return (
|
|
84
91
|
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
85
92
|
<section className="flex flex-col gap-2" aria-label="Suppress contacts">
|
|
86
|
-
<
|
|
87
|
-
<
|
|
93
|
+
<span className="text-sm font-medium leading-none">Who to suppress</span>
|
|
94
|
+
<ContactPicker multiple label="Contacts to suppress" selected={chosen} onSelectedChange={setChosen} />
|
|
95
|
+
<details className="text-sm">
|
|
96
|
+
<summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
|
|
97
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
98
|
+
<Label htmlFor="suppress-ids">Suppress contacts by id</Label>
|
|
99
|
+
<Textarea id="suppress-ids" rows={2} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
|
|
100
|
+
</div>
|
|
101
|
+
</details>
|
|
88
102
|
<div className="flex flex-wrap items-center gap-2">
|
|
89
103
|
<select aria-label="Channel to suppress" className="h-9 rounded-md border bg-background px-2 text-sm" value={addChannel} onChange={(e) => setAddChannel(e.target.value)}>
|
|
90
104
|
<option value="all">All channels</option><option value="email">Email</option><option value="phone">Phone</option><option value="linkedin">LinkedIn</option>
|
|
@@ -97,8 +111,15 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
97
111
|
</section>
|
|
98
112
|
|
|
99
113
|
<section className="flex items-end gap-2" aria-label="Check a contact">
|
|
100
|
-
<div className="flex flex-col gap-1"
|
|
101
|
-
|
|
114
|
+
<div className="flex flex-col gap-1">
|
|
115
|
+
<span className="text-sm font-medium leading-none">Check one contact</span>
|
|
116
|
+
<ContactPicker label="Contact to check" value={checkChosen} onChange={(c) => { setCheckChosen(c); setCheckId(""); }} />
|
|
117
|
+
<details className="text-sm">
|
|
118
|
+
<summary className="cursor-pointer text-muted-foreground">Check by id instead</summary>
|
|
119
|
+
<div className="mt-1 flex flex-col gap-1"><Label htmlFor="check-id">Check one contact</Label><Input id="check-id" inputMode="numeric" placeholder="Contact id" className="w-40" value={checkId} onChange={(e) => { setCheckId(e.target.value); setCheckChosen(null); }} /></div>
|
|
120
|
+
</details>
|
|
121
|
+
</div>
|
|
122
|
+
<Button variant="outline" size="sm" loading={check.loading} disabled={!checkTarget} onClick={() => void check.mutate(checkTarget)}>Check</Button>
|
|
102
123
|
{check.data ? <p role="status" className="text-sm">{check.data.data.is_suppressed ? `Suppressed on: ${check.data.data.active_channels.join(", ")}` : "Not suppressed"}</p> : null}
|
|
103
124
|
{check.error ? <ApiError error={check.error} title="Could not check that contact" /> : null}
|
|
104
125
|
</section>
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* User flows:
|
|
11
11
|
* - filter the ledger by channel and reason, page through it, see how many are suppressed on each channel
|
|
12
|
-
* - Suppress contacts:
|
|
12
|
+
* - Suppress contacts: search and tick people (ContactPicker), or pick them from the CRM table, or paste ids under
|
|
13
|
+
* "Paste ids instead"; choose the channel (default all) and a reason
|
|
13
14
|
* - open a ledger row: the contact's record plus its live suppression status (read-only check)
|
|
14
|
-
* - check any contact
|
|
15
|
+
* - check any contact without opening it (pick them by name; no ids to type)
|
|
15
16
|
* - tick rows and Reinstate (confirmed: it lets sequences, campaigns and newsletters contact them again)
|
|
16
17
|
*
|
|
17
18
|
* SDK operations, in order:
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
|
|
25
26
|
import { useEffect, useState } from "react";
|
|
26
27
|
import type { ReactNode } from "react";
|
|
27
|
-
import { BadgeCheck, Ban,
|
|
28
|
+
import { BadgeCheck, Ban, ShieldOff, Undo2 } from "lucide-react";
|
|
28
29
|
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
29
30
|
import { ApiError } from "@/g8/components/api-error";
|
|
30
31
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -37,6 +38,8 @@ import { SectionCard } from "@/g8/components/screen/section-card";
|
|
|
37
38
|
import { SplitView } from "@/g8/components/screen/split-view";
|
|
38
39
|
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
39
40
|
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
41
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
42
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
40
43
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
41
44
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
42
45
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
@@ -85,6 +88,7 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
85
88
|
const full = view === "full";
|
|
86
89
|
const [adding, setAdding] = useState(false);
|
|
87
90
|
const [idsText, setIdsText] = useState("");
|
|
91
|
+
const [pickedContacts, setPickedContacts] = useState<ContactOption[]>([]);
|
|
88
92
|
const [addChannel, setAddChannel] = useState("all");
|
|
89
93
|
const [reason, setReason] = useState("");
|
|
90
94
|
const [picked, setPicked] = useState<Set<number>>(new Set());
|
|
@@ -92,8 +96,7 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
92
96
|
/** Set when Reinstate is started from a row's panel: the confirm acts on that contact only and leaves the ticked rows alone. */
|
|
93
97
|
const [panelTarget, setPanelTarget] = useState<number | null>(null);
|
|
94
98
|
const [reinstateReason, setReinstateReason] = useState("");
|
|
95
|
-
const [
|
|
96
|
-
const [checked, setChecked] = useState<number | null>(null);
|
|
99
|
+
const [checked, setChecked] = useState<ContactOption | null>(null);
|
|
97
100
|
|
|
98
101
|
const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
|
|
99
102
|
const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
|
|
@@ -101,13 +104,13 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
101
104
|
useEffect(() => { setPicked((p) => new Set([...p].filter((id) => rows.some((r) => r.contact_id === id)))); }, [ledger.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
102
105
|
|
|
103
106
|
const add = useG8Mutation((v: { ids: number[] }) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: v.ids, channel: addChannel, reason: reason.trim() || undefined }), {
|
|
104
|
-
onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
|
|
107
|
+
onSuccess: () => { setIdsText(""); setPickedContacts([]); setReason(""); void ledger.refetch(); },
|
|
105
108
|
});
|
|
106
109
|
const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
|
|
107
110
|
onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); if (panelTarget != null) { const done = panelTarget; setPicked((p) => { const n = new Set(p); n.delete(done); return n; }); } else setPicked(new Set()); setPanelTarget(null); void ledger.refetch(); },
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
const ids = parseIds(idsText);
|
|
113
|
+
const ids = [...new Set([...pickedContacts.map((c) => c.id), ...parseIds(idsText)])];
|
|
111
114
|
const chosen = panelTarget != null ? [panelTarget] : [...picked];
|
|
112
115
|
const togglePick = (id: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(id)) n.add(id); return n; });
|
|
113
116
|
const byChannel = (c: string) => rows.filter((r) => r.channel === c).length;
|
|
@@ -205,9 +208,8 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
205
208
|
<select aria-label="Filter by reason" className={SELECT} value={category} onChange={(e) => { setCategory(e.target.value); resetPage(); }}>
|
|
206
209
|
<option value="">Any reason</option><option value="contact_initiated">Contact asked</option><option value="org_initiated">Our team</option><option value="system">System</option>
|
|
207
210
|
</select>
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
{checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked} contactId={checked} /><span className="text-xs text-muted-foreground">#{checked}</span></span> : null}
|
|
211
|
+
<ContactPicker label="Contact to check" placeholder="Check a contact" value={checked} onChange={setChecked} />
|
|
212
|
+
{checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked.id} contactId={checked.id} /><span className="text-xs text-muted-foreground">{checked.name}</span></span> : null}
|
|
211
213
|
</>)}
|
|
212
214
|
actions={<Button variant="outline" size="sm" disabled={chosen.length === 0} onClick={() => { setPanelTarget(null); setConfirmReinstate(true); }}><BadgeCheck className="mr-1 size-4" aria-hidden />Reinstate {chosen.length || ""}</Button>}
|
|
213
215
|
/>
|
|
@@ -224,8 +226,15 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
224
226
|
<div className="flex min-w-40 flex-1 flex-col gap-1"><Label htmlFor="suppress-reason">Reason (optional)</Label><Input id="suppress-reason" value={reason} onChange={(e) => setReason(e.target.value)} /></div>
|
|
225
227
|
</div>
|
|
226
228
|
<div className="flex flex-col gap-2">
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
+
<span className="text-sm font-medium leading-none">Search and add people</span>
|
|
230
|
+
<ContactPicker multiple label="Contacts to suppress" selected={pickedContacts} onSelectedChange={setPickedContacts} />
|
|
231
|
+
<details className="text-sm">
|
|
232
|
+
<summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
|
|
233
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
234
|
+
<Label htmlFor="suppress-ids">Contact ids</Label>
|
|
235
|
+
<Textarea id="suppress-ids" rows={3} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
|
|
236
|
+
</div>
|
|
237
|
+
</details>
|
|
229
238
|
<Button size="sm" loading={add.loading} disabled={ids.length === 0} onClick={() => void add.mutate({ ids })}>{ids.length ? `Suppress ${plural(ids.length, "contact")}` : "Suppress contacts"}</Button>
|
|
230
239
|
</div>
|
|
231
240
|
<div className="flex flex-col gap-2">
|
|
@@ -157,6 +157,10 @@
|
|
|
157
157
|
{
|
|
158
158
|
"file": "lib/mailboxes-utils.ts",
|
|
159
159
|
"purpose": "Mailbox and domain row types, credit estimate constants, status colors and idempotency-key helper."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"file": "lib/use-provider-domains.ts",
|
|
163
|
+
"purpose": "Every sending domain, read page after page (the domains API pages) instead of one capped call."
|
|
160
164
|
}
|
|
161
165
|
],
|
|
162
166
|
"recipes": [
|
|
@@ -25,6 +25,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
25
25
|
import { ApiError } from "@/g8/components/api-error";
|
|
26
26
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
27
|
import { DEFAULT_MAILBOX_CREDITS, GRAPH8_APP_MAILBOXES_PATH, listOf, newIdempotencyKey, objectOf } from "../lib/mailboxes-utils";
|
|
28
|
+
import { useProviderDomains } from "../lib/use-provider-domains";
|
|
28
29
|
import type { DomainRow } from "../lib/mailboxes-utils";
|
|
29
30
|
import { plural, pluralWord } from "@/g8/lib/plural";
|
|
30
31
|
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
@@ -135,7 +136,7 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
|
|
|
135
136
|
</Button>
|
|
136
137
|
{connect.error ? <ApiError error={connect.error} onRetry={connect.reset} /> : null}
|
|
137
138
|
{result ? (
|
|
138
|
-
<p role="status" className="text-sm text-
|
|
139
|
+
<p role="status" className="text-sm text-foreground">
|
|
139
140
|
{result}
|
|
140
141
|
</p>
|
|
141
142
|
) : null}
|
|
@@ -145,7 +146,7 @@ function ImapForm({ onConnected }: { onConnected?: (id: number | string) => void
|
|
|
145
146
|
|
|
146
147
|
function BuyForm({ onPurchased, domainId: initialDomainId }: { onPurchased?: (id: number | string) => void; domainId?: number | string }) {
|
|
147
148
|
const brand = useBrand();
|
|
148
|
-
const domains =
|
|
149
|
+
const domains = useProviderDomains([]);
|
|
149
150
|
const pricing = useG8Query((signal) => g8Get("/provider-domains/pricing", undefined, { signal }), []);
|
|
150
151
|
const [domainId, setDomainId] = useState(initialDomainId != null ? String(initialDomainId) : "");
|
|
151
152
|
const [usernames, setUsernames] = useState("");
|
|
@@ -170,19 +171,19 @@ function BuyForm({ onPurchased, domainId: initialDomainId }: { onPurchased?: (id
|
|
|
170
171
|
setDone(`Accepted ${plural(Number(res.count ?? names.length), "mailbox", "mailboxes")}; provisioning continues in the background.`);
|
|
171
172
|
onPurchased?.(domainId);
|
|
172
173
|
});
|
|
173
|
-
const connected =
|
|
174
|
+
const connected = domains.items.filter((d) => d.provider_connected);
|
|
174
175
|
const ready = domainId && names.length > 0 && person.first_name.trim() && person.last_name.trim();
|
|
175
176
|
|
|
176
177
|
return (
|
|
177
178
|
<div className="space-y-3">
|
|
178
179
|
{domains.error ? <ApiError error={domains.error} onRetry={() => void domains.refetch()} /> : null}
|
|
179
180
|
{!domains.loading && connected.length === 0 ? (
|
|
180
|
-
<p className="text-sm text-
|
|
181
|
+
<p className="text-sm text-muted-foreground">No connected domain yet. Buy and connect one with DomainPurchase first.</p>
|
|
181
182
|
) : (
|
|
182
183
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
183
184
|
<select
|
|
184
185
|
aria-label="Domain"
|
|
185
|
-
className="h-9 rounded-md border border-
|
|
186
|
+
className="h-9 rounded-md border border-input px-2 text-sm"
|
|
186
187
|
value={domainId}
|
|
187
188
|
onChange={(e) => setDomainId(e.target.value)}
|
|
188
189
|
>
|
|
@@ -195,7 +196,7 @@ function BuyForm({ onPurchased, domainId: initialDomainId }: { onPurchased?: (id
|
|
|
195
196
|
</select>
|
|
196
197
|
<select
|
|
197
198
|
aria-label="Platform"
|
|
198
|
-
className="h-9 rounded-md border border-
|
|
199
|
+
className="h-9 rounded-md border border-input px-2 text-sm"
|
|
199
200
|
value={platform}
|
|
200
201
|
onChange={(e) => setPlatform(e.target.value as "GOOGLE" | "MICROSOFT")}
|
|
201
202
|
>
|
|
@@ -231,7 +232,7 @@ function BuyForm({ onPurchased, domainId: initialDomainId }: { onPurchased?: (id
|
|
|
231
232
|
</Button>
|
|
232
233
|
{buy.error ? <ApiError error={buy.error} onRetry={buy.reset} /> : null}
|
|
233
234
|
{done ? (
|
|
234
|
-
<p role="status" className="text-sm text-
|
|
235
|
+
<p role="status" className="text-sm text-positive-foreground">
|
|
235
236
|
{done}
|
|
236
237
|
</p>
|
|
237
238
|
) : null}
|
|
@@ -276,7 +277,7 @@ export function ConnectMailbox({ appUrl: appUrlProp, onConnected, onPurchased, i
|
|
|
276
277
|
</div>
|
|
277
278
|
{mode === "imap" ? <ImapForm onConnected={onConnected} /> : null}
|
|
278
279
|
{mode === "oauth" ? (
|
|
279
|
-
<div className="space-y-2 rounded border border-
|
|
280
|
+
<div className="space-y-2 rounded border border-border p-3 text-sm">
|
|
280
281
|
<p>
|
|
281
282
|
Google and Microsoft mailboxes connect through a sign-in screen that only runs inside the {brand.linkLabel} app. Connect it there, then
|
|
282
283
|
come back: it appears in your mailbox list.
|
|
@@ -18,6 +18,7 @@ import { useState } from "react";
|
|
|
18
18
|
import type { ReactNode } from "react";
|
|
19
19
|
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
20
20
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
21
22
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
23
|
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
23
24
|
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
@@ -27,6 +28,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
27
28
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
29
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
30
|
import { fieldItems, listOf, objectOf } from "../lib/mailboxes-utils";
|
|
31
|
+
import { useProviderDomains } from "../lib/use-provider-domains";
|
|
30
32
|
import type { DomainRow, MailboxRow } from "../lib/mailboxes-utils";
|
|
31
33
|
|
|
32
34
|
export interface DomainDetailProps {
|
|
@@ -46,13 +48,16 @@ export interface DomainDetailProps {
|
|
|
46
48
|
|
|
47
49
|
type Pending = "connect" | "propagation";
|
|
48
50
|
|
|
51
|
+
/** The mailboxes API has no paging: a full read is announced where the mailboxes are listed. */
|
|
52
|
+
const MAILBOX_LIMIT = 200;
|
|
53
|
+
|
|
49
54
|
export function DomainDetail({ domainId, onClose, onExpand, onCollapse, onChanged, onBuyMailboxes, onOpenMailbox, renderActions, className }: DomainDetailProps) {
|
|
50
55
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
51
56
|
const [note, setNote] = useState<string | null>(null);
|
|
52
|
-
const domains =
|
|
53
|
-
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit:
|
|
57
|
+
const domains = useProviderDomains([domainId]);
|
|
58
|
+
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT }, { signal }), [domainId]);
|
|
54
59
|
|
|
55
|
-
const row =
|
|
60
|
+
const row = domains.items.find((d) => String(d.id) === String(domainId));
|
|
56
61
|
const run = useG8Mutation(async (p: Pending) => {
|
|
57
62
|
if (p === "connect") {
|
|
58
63
|
await g8Post(`/provider-domains/${domainId}/connect-provider`, {});
|
|
@@ -67,7 +72,7 @@ export function DomainDetail({ domainId, onClose, onExpand, onCollapse, onChange
|
|
|
67
72
|
}, { onError: () => setPending(null) });
|
|
68
73
|
|
|
69
74
|
if (domains.error) return <ApiError error={domains.error} title="Could not load the domain" onRetry={() => void domains.refetch()} className={className} />;
|
|
70
|
-
if (domains.loading && !domains.
|
|
75
|
+
if (domains.loading && !domains.loaded) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
71
76
|
if (!row) return <p className={`p-4 text-sm ${className ?? ""}`}>This domain no longer exists.</p>;
|
|
72
77
|
|
|
73
78
|
const onDomain = listOf<MailboxRow>(mailboxes.data).filter((m) => (m.email ?? "").toLowerCase().endsWith(`@${row.domain.toLowerCase()}`));
|
|
@@ -133,6 +138,7 @@ export function DomainDetail({ domainId, onClose, onExpand, onCollapse, onChange
|
|
|
133
138
|
))}
|
|
134
139
|
</ul>
|
|
135
140
|
)}
|
|
141
|
+
<FirstNotice shown={listOf<MailboxRow>(mailboxes.data).length} limit={MAILBOX_LIMIT} noun="mailboxes" />
|
|
136
142
|
</SectionCard>
|
|
137
143
|
),
|
|
138
144
|
},
|
|
@@ -24,9 +24,11 @@ import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
|
24
24
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
26
|
import { ApiError } from "@/g8/components/api-error";
|
|
27
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
27
28
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
28
29
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
30
|
import { DEFAULT_DOMAIN_CREDITS, listOf, newIdempotencyKey, objectOf, statusClass } from "../lib/mailboxes-utils";
|
|
31
|
+
import { useProviderDomains } from "../lib/use-provider-domains";
|
|
30
32
|
import type { DomainRow } from "../lib/mailboxes-utils";
|
|
31
33
|
import { plural } from "@/g8/lib/plural";
|
|
32
34
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
@@ -66,7 +68,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
66
68
|
const [note, setNote] = useState<string | null>(null);
|
|
67
69
|
const idemKey = useRef<{ sig: string; key: string } | null>(null);
|
|
68
70
|
|
|
69
|
-
const domains =
|
|
71
|
+
const domains = useProviderDomains([]);
|
|
70
72
|
const suggest = useG8Mutation(async () => {
|
|
71
73
|
setNames(suggestionsOf(await g8Post("/provider-domains/generate-domains", { keyword: keyword.trim(), count: 10 })));
|
|
72
74
|
setSelected([]);
|
|
@@ -106,7 +108,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
106
108
|
const toggle = (d: string) => setSelected((s) => (s.includes(d) ? s.filter((x) => x !== d) : [...s, d]));
|
|
107
109
|
const buyable = selected.filter((d) => avail[d]?.available);
|
|
108
110
|
const contactOk = contact.first_name.trim() && contact.last_name.trim() && /\S+@\S+/.test(contact.email);
|
|
109
|
-
const rows =
|
|
111
|
+
const rows = domains.items;
|
|
110
112
|
|
|
111
113
|
const dialog = (() => {
|
|
112
114
|
if (!pending) return null;
|
|
@@ -141,8 +143,8 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
141
143
|
|
|
142
144
|
return (
|
|
143
145
|
<div className={`space-y-5 ${className ?? ""}`}>
|
|
144
|
-
<section className="space-y-2 rounded border border-
|
|
145
|
-
<h3 className="text-sm font-
|
|
146
|
+
<section className="space-y-2 rounded border border-border p-3">
|
|
147
|
+
<h3 className="text-sm font-medium">Find a domain</h3>
|
|
146
148
|
<form
|
|
147
149
|
className="flex gap-2"
|
|
148
150
|
onSubmit={(e) => {
|
|
@@ -165,7 +167,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
165
167
|
{d}
|
|
166
168
|
{avail[d] ? (
|
|
167
169
|
<span
|
|
168
|
-
className={`rounded px-1.5 text-xs ${avail[d].available ? "bg-
|
|
170
|
+
className={`rounded px-1.5 text-xs ${avail[d].available ? "bg-positive text-positive-foreground" : "bg-error text-error-foreground"}`}
|
|
169
171
|
>
|
|
170
172
|
{avail[d].available
|
|
171
173
|
? `available${avail[d].price != null ? ` ${avail[d].price}` : ""}`
|
|
@@ -189,8 +191,8 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
189
191
|
</Button>
|
|
190
192
|
</section>
|
|
191
193
|
|
|
192
|
-
<section className="space-y-2 rounded border border-
|
|
193
|
-
<h3 className="text-sm font-
|
|
194
|
+
<section className="space-y-2 rounded border border-border p-3">
|
|
195
|
+
<h3 className="text-sm font-medium">Registrant</h3>
|
|
194
196
|
<div className="grid gap-2 sm:grid-cols-3">
|
|
195
197
|
<Input
|
|
196
198
|
aria-label="First name"
|
|
@@ -217,7 +219,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
217
219
|
</section>
|
|
218
220
|
|
|
219
221
|
{note ? (
|
|
220
|
-
<p role="status" className="text-sm text-
|
|
222
|
+
<p role="status" className="text-sm text-foreground">
|
|
221
223
|
{note}
|
|
222
224
|
</p>
|
|
223
225
|
) : null}
|
|
@@ -226,18 +228,18 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
226
228
|
<section aria-label="Your domains">
|
|
227
229
|
{domains.error ? (
|
|
228
230
|
<ApiError error={domains.error} onRetry={() => void domains.refetch()} />
|
|
229
|
-
) : domains.loading && !domains.
|
|
231
|
+
) : domains.loading && !domains.loaded ? (
|
|
230
232
|
<Skeleton className="h-16 w-full" />
|
|
231
233
|
) : rows.length === 0 ? (
|
|
232
234
|
<EmptyState title="No sending domains yet" description={`Buy one above, or add a domain you already own in the ${brand.linkLabel} app.`} />
|
|
233
235
|
) : (
|
|
234
|
-
<ul className="divide-y rounded border border-
|
|
236
|
+
<ul className="divide-y rounded border border-border">
|
|
235
237
|
{rows.map((d) => (
|
|
236
238
|
<li key={d.id} className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
|
|
237
239
|
<span>
|
|
238
240
|
<span className="font-medium">{d.domain}</span>{" "}
|
|
239
241
|
<span className={`rounded px-1.5 text-xs ${statusClass(d.status)}`}>{d.status ?? "unknown"}</span>{" "}
|
|
240
|
-
<span className="text-xs text-
|
|
242
|
+
<span className="text-xs text-muted-foreground">{d.mailbox_count ?? 0} mailboxes</span>
|
|
241
243
|
</span>
|
|
242
244
|
<span className="flex gap-1">
|
|
243
245
|
{!d.provider_connected && !d.is_external ? (
|
|
@@ -254,6 +256,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
254
256
|
))}
|
|
255
257
|
</ul>
|
|
256
258
|
)}
|
|
259
|
+
<FirstNotice shown={rows.length} limit={rows.length} force={domains.truncated} noun="domains" />
|
|
257
260
|
</section>
|
|
258
261
|
|
|
259
262
|
{pending && dialog ? (
|
|
@@ -29,6 +29,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
29
29
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
30
|
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
31
31
|
import { fieldItems, listOf, objectOf } from "../lib/mailboxes-utils";
|
|
32
|
+
import { useProviderDomains } from "../lib/use-provider-domains";
|
|
32
33
|
import type { DomainRow, MailboxRow } from "../lib/mailboxes-utils";
|
|
33
34
|
import { WarmupHealth } from "./warmup-health";
|
|
34
35
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
@@ -59,7 +60,7 @@ export function MailboxDetail({ mailboxId, tab, onTabChange, onClose, onExpand,
|
|
|
59
60
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
60
61
|
const [testNote, setTestNote] = useState<string | null>(null);
|
|
61
62
|
const q = useG8Query((signal) => g8Get(`/mailboxes/${mailboxId}`, undefined, { signal }), [mailboxId]);
|
|
62
|
-
const domains =
|
|
63
|
+
const domains = useProviderDomains([]);
|
|
63
64
|
|
|
64
65
|
const test = useG8Mutation(async () => {
|
|
65
66
|
const r = objectOf(await g8Post(`/mailboxes/${mailboxId}/test-connection`, {})) as TestResult;
|
|
@@ -84,7 +85,7 @@ export function MailboxDetail({ mailboxId, tab, onTabChange, onClose, onExpand,
|
|
|
84
85
|
const raw = objectOf(q.data);
|
|
85
86
|
const m = { id: String(mailboxId), email: null, ...raw } as MailboxRow;
|
|
86
87
|
const domain = (m.email ?? "").split("@")[1]?.toLowerCase();
|
|
87
|
-
const domainRow =
|
|
88
|
+
const domainRow = domains.items.find((d) => d.domain?.toLowerCase() === domain);
|
|
88
89
|
const status = m.connection_status ?? "unknown";
|
|
89
90
|
const name = m.email ?? `Mailbox ${mailboxId}`;
|
|
90
91
|
|
|
@@ -22,9 +22,13 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
22
22
|
import { ApiError } from "@/g8/components/api-error";
|
|
23
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
25
26
|
import { listOf, objectOf, statusClass } from "../lib/mailboxes-utils";
|
|
26
27
|
import type { MailboxRow } from "../lib/mailboxes-utils";
|
|
27
28
|
|
|
29
|
+
/** The read cap. The mailboxes API has no paging, so a full read is announced ("Showing the first 200"). */
|
|
30
|
+
const MAILBOX_LIMIT = 200;
|
|
31
|
+
|
|
28
32
|
export interface MailboxListProps {
|
|
29
33
|
includeArchived?: boolean;
|
|
30
34
|
/** Called when a row is opened (for example to show WarmupHealth for it). */
|
|
@@ -47,7 +51,7 @@ export function MailboxList({ includeArchived = false, onSelect, emptyAction, cl
|
|
|
47
51
|
const [tests, setTests] = useState<Record<string, string>>({});
|
|
48
52
|
const [testing, setTesting] = useState<ReadonlySet<string>>(new Set());
|
|
49
53
|
const list = useG8Query(
|
|
50
|
-
(signal) => g8Get("/mailboxes", { limit:
|
|
54
|
+
(signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT, include_archived: includeArchived }, { signal }),
|
|
51
55
|
[includeArchived],
|
|
52
56
|
);
|
|
53
57
|
|
|
@@ -114,13 +118,13 @@ export function MailboxList({ includeArchived = false, onSelect, emptyAction, cl
|
|
|
114
118
|
<button type="button" className="text-left font-medium hover:underline" onClick={() => onSelect?.(m)}>
|
|
115
119
|
{m.email ?? `Mailbox ${m.id}`}
|
|
116
120
|
</button>
|
|
117
|
-
<div className="text-xs text-
|
|
121
|
+
<div className="text-xs text-muted-foreground">
|
|
118
122
|
{m.display_name ?? ""}
|
|
119
123
|
{m.provider ? ` - ${m.provider}` : ""}
|
|
120
124
|
{m.is_archived ? " - archived" : ""}
|
|
121
125
|
</div>
|
|
122
126
|
{tests[m.id] ? (
|
|
123
|
-
<div role="status" className="text-xs text-
|
|
127
|
+
<div role="status" className="text-xs text-foreground">
|
|
124
128
|
{tests[m.id]}
|
|
125
129
|
</div>
|
|
126
130
|
) : null}
|
|
@@ -163,6 +167,7 @@ export function MailboxList({ includeArchived = false, onSelect, emptyAction, cl
|
|
|
163
167
|
))}
|
|
164
168
|
</TableBody>
|
|
165
169
|
</Table>
|
|
170
|
+
<FirstNotice shown={rows.length} limit={MAILBOX_LIMIT} noun="mailboxes" />
|
|
166
171
|
{pending ? (
|
|
167
172
|
<ConfirmSpendDialog
|
|
168
173
|
open
|
|
@@ -38,7 +38,9 @@ import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
|
38
38
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
39
39
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
40
40
|
import { listOf, objectOf } from "../lib/mailboxes-utils";
|
|
41
|
+
import { useProviderDomains } from "../lib/use-provider-domains";
|
|
41
42
|
import type { DomainRow, MailboxRow } from "../lib/mailboxes-utils";
|
|
43
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
42
44
|
import { ConnectMailbox } from "./connect-mailbox";
|
|
43
45
|
import { DomainPurchase } from "./domain-purchase";
|
|
44
46
|
import { DomainDetail } from "./domain-detail";
|
|
@@ -83,11 +85,12 @@ export function fleetStats(summary: Record<string, unknown>): StatItem[] {
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
function DomainList({ version, selectedId, onOpen }: { version: number; selectedId: string; onOpen: (d: DomainRow) => void }) {
|
|
86
|
-
const q =
|
|
87
|
-
const rows =
|
|
88
|
-
if (q.loading && !q.
|
|
88
|
+
const q = useProviderDomains([version]);
|
|
89
|
+
const rows = q.items;
|
|
90
|
+
if (q.loading && !q.loaded) return <Skeleton className="h-24 w-full" />;
|
|
89
91
|
if (rows.length === 0) return null;
|
|
90
92
|
return (
|
|
93
|
+
<>
|
|
91
94
|
<ul aria-label="Your domains" className="divide-y rounded-md border">
|
|
92
95
|
{rows.map((d) => (
|
|
93
96
|
<li key={d.id} className={`flex items-center justify-between px-3 py-2 text-sm ${String(d.id) === selectedId ? "bg-muted" : ""}`}>
|
|
@@ -99,6 +102,8 @@ function DomainList({ version, selectedId, onOpen }: { version: number; selected
|
|
|
99
102
|
</li>
|
|
100
103
|
))}
|
|
101
104
|
</ul>
|
|
105
|
+
<FirstNotice shown={rows.length} limit={rows.length} force={q.truncated} noun="domains" />
|
|
106
|
+
</>
|
|
102
107
|
);
|
|
103
108
|
}
|
|
104
109
|
|