@graph8/sdk 0.361.0 → 0.370.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DomainDetail: one sending domain, complete. Every field, the mailboxes on it, and every action: connect it
|
|
5
|
+
* to mailbox provisioning, check DNS propagation, buy mailboxes on it.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listProviderDomains GET /provider-domains (read; the row for this domain)
|
|
9
|
+
* 2. listMailboxes GET /mailboxes (read; mailboxes whose address ends with the domain)
|
|
10
|
+
* 3. connectProvider POST /provider-domains/{domain_id}/connect-provider (billable tier: confirmed)
|
|
11
|
+
* 4. checkDomainPropagation POST /provider-domains/{domain_id}/check-propagation (billable tier: confirmed)
|
|
12
|
+
* Buying mailboxes is the ConnectMailbox "Buy" tab, opened by `onBuyMailboxes` (confirmed with a credit estimate there).
|
|
13
|
+
*
|
|
14
|
+
* Tiers: 3 and 4 are billable in the contract (no credit estimate is known), so each opens ConfirmSpendDialog.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import type { ReactNode } from "react";
|
|
19
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
23
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { fieldItems, listOf, objectOf } from "../lib/mailboxes-utils";
|
|
30
|
+
import type { DomainRow, MailboxRow } from "../lib/mailboxes-utils";
|
|
31
|
+
|
|
32
|
+
export interface DomainDetailProps {
|
|
33
|
+
domainId: string | number;
|
|
34
|
+
onClose?: () => void;
|
|
35
|
+
/** Full-page mode: show an "Open full page" (onExpand) or "Back to split view" (onCollapse) button. */
|
|
36
|
+
onExpand?: () => void;
|
|
37
|
+
onCollapse?: () => void;
|
|
38
|
+
/** Called after a connect so a list can refresh. */
|
|
39
|
+
onChanged?: () => void;
|
|
40
|
+
/** Open the Buy tab of ConnectMailbox for this domain (a screen shows it in a sheet). */
|
|
41
|
+
onBuyMailboxes?: (domain: DomainRow) => void;
|
|
42
|
+
onOpenMailbox?: (mailbox: MailboxRow) => void;
|
|
43
|
+
renderActions?: (domain: DomainRow) => ReactNode;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type Pending = "connect" | "propagation";
|
|
48
|
+
|
|
49
|
+
export function DomainDetail({ domainId, onClose, onExpand, onCollapse, onChanged, onBuyMailboxes, onOpenMailbox, renderActions, className }: DomainDetailProps) {
|
|
50
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
51
|
+
const [note, setNote] = useState<string | null>(null);
|
|
52
|
+
const domains = useG8Query((signal) => g8Get("/provider-domains", { page: 1, limit: 100 }, { signal }), [domainId]);
|
|
53
|
+
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: 200 }, { signal }), [domainId]);
|
|
54
|
+
|
|
55
|
+
const row = listOf<DomainRow>(domains.data).find((d) => String(d.id) === String(domainId));
|
|
56
|
+
const run = useG8Mutation(async (p: Pending) => {
|
|
57
|
+
if (p === "connect") {
|
|
58
|
+
await g8Post(`/provider-domains/${domainId}/connect-provider`, {});
|
|
59
|
+
setNote(`${row?.domain ?? "The domain"} is connecting. Check propagation in a few minutes.`);
|
|
60
|
+
await domains.refetch();
|
|
61
|
+
onChanged?.();
|
|
62
|
+
} else {
|
|
63
|
+
const status = objectOf(await g8Post(`/provider-domains/${domainId}/check-propagation`, {}));
|
|
64
|
+
setNote(status.propagated ? `${row?.domain} has propagated.` : `${row?.domain} has not propagated yet.`);
|
|
65
|
+
}
|
|
66
|
+
setPending(null);
|
|
67
|
+
}, { onError: () => setPending(null) });
|
|
68
|
+
|
|
69
|
+
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.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
71
|
+
if (!row) return <p className={`p-4 text-sm ${className ?? ""}`}>This domain no longer exists.</p>;
|
|
72
|
+
|
|
73
|
+
const onDomain = listOf<MailboxRow>(mailboxes.data).filter((m) => (m.email ?? "").toLowerCase().endsWith(`@${row.domain.toLowerCase()}`));
|
|
74
|
+
return (
|
|
75
|
+
<DetailPanel
|
|
76
|
+
className={className}
|
|
77
|
+
title={row.domain}
|
|
78
|
+
subtitle={row.source ?? undefined}
|
|
79
|
+
badges={
|
|
80
|
+
<>
|
|
81
|
+
<StatusPill tone={row.provider_connected ? "positive" : "warning"} dot>{row.provider_connected ? "Connected" : "Not connected"}</StatusPill>
|
|
82
|
+
{row.status ? <StatusPill tone="neutral">{row.status}</StatusPill> : null}
|
|
83
|
+
</>
|
|
84
|
+
}
|
|
85
|
+
actions={
|
|
86
|
+
<>
|
|
87
|
+
{!row.provider_connected ? <Button size="sm" variant="outline" onClick={() => setPending("connect")}>Connect domain</Button> : null}
|
|
88
|
+
<Button size="sm" variant="outline" onClick={() => setPending("propagation")}>Check DNS</Button>
|
|
89
|
+
<Button size="sm" disabled={!row.provider_connected || !onBuyMailboxes} onClick={() => onBuyMailboxes?.(row)}>Buy mailboxes</Button>
|
|
90
|
+
{renderActions?.(row)}
|
|
91
|
+
<ConfirmSpendDialog
|
|
92
|
+
open={pending !== null}
|
|
93
|
+
tier="billable"
|
|
94
|
+
title={pending === "connect" ? `Connect ${row.domain}` : `Check DNS propagation for ${row.domain}`}
|
|
95
|
+
description={pending === "connect" ? "Connects the domain to mailbox provisioning. Required before buying mailboxes on it." : "Asks the provider whether the nameservers have propagated."}
|
|
96
|
+
confirmLabel={pending === "connect" ? "Connect domain" : "Check propagation"}
|
|
97
|
+
confirming={run.loading}
|
|
98
|
+
onCancel={() => setPending(null)}
|
|
99
|
+
onConfirm={() => pending && void run.mutate(pending)}
|
|
100
|
+
/>
|
|
101
|
+
</>
|
|
102
|
+
}
|
|
103
|
+
summary={note ? <p role="status" className="text-sm">{note}</p> : undefined}
|
|
104
|
+
onClose={onClose}
|
|
105
|
+
onExpand={onExpand}
|
|
106
|
+
onCollapse={onCollapse}
|
|
107
|
+
tabs={[
|
|
108
|
+
{
|
|
109
|
+
id: "details",
|
|
110
|
+
label: "Details",
|
|
111
|
+
content: (
|
|
112
|
+
<div className="space-y-4">
|
|
113
|
+
{run.error ? <ApiError error={run.error} onRetry={run.reset} /> : null}
|
|
114
|
+
<KeyValueList columns={2} items={fieldItems(row as unknown as Record<string, unknown>)} />
|
|
115
|
+
</div>
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "mailboxes",
|
|
120
|
+
label: "Mailboxes",
|
|
121
|
+
count: onDomain.length,
|
|
122
|
+
content: (
|
|
123
|
+
<SectionCard title="Mailboxes on this domain" description="Open one for warmup and health.">
|
|
124
|
+
{onDomain.length === 0 ? (
|
|
125
|
+
<p className="text-sm text-muted-foreground">{row.provider_connected ? "No mailboxes yet. Buy some with the button above." : "Connect the domain first, then buy mailboxes on it."}</p>
|
|
126
|
+
) : (
|
|
127
|
+
<ul className="divide-y">
|
|
128
|
+
{onDomain.map((m) => (
|
|
129
|
+
<li key={m.id} className="flex items-center justify-between py-2 text-sm">
|
|
130
|
+
<Button variant="ghost" size="sm" onClick={() => onOpenMailbox?.(m)}>{m.email}</Button>
|
|
131
|
+
<StatusPill tone={m.connection_status === "active" ? "positive" : "warning"} dot>{m.connection_status ?? "unknown"}</StatusPill>
|
|
132
|
+
</li>
|
|
133
|
+
))}
|
|
134
|
+
</ul>
|
|
135
|
+
)}
|
|
136
|
+
</SectionCard>
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
]}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -28,6 +28,8 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
28
28
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
29
|
import { DEFAULT_DOMAIN_CREDITS, listOf, newIdempotencyKey, objectOf, statusClass } from "../lib/mailboxes-utils";
|
|
30
30
|
import type { DomainRow } from "../lib/mailboxes-utils";
|
|
31
|
+
import { plural } from "@/g8/lib/plural";
|
|
32
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
33
|
|
|
32
34
|
export interface DomainPurchaseProps {
|
|
33
35
|
/** Called after a purchase was accepted (provisioning continues in the background). */
|
|
@@ -54,6 +56,7 @@ function suggestionsOf(envelope: unknown): string[] {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps) {
|
|
59
|
+
const brand = useBrand();
|
|
57
60
|
const [keyword, setKeyword] = useState("");
|
|
58
61
|
const [names, setNames] = useState<string[]>([]);
|
|
59
62
|
const [selected, setSelected] = useState<string[]>([]);
|
|
@@ -109,7 +112,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
109
112
|
if (!pending) return null;
|
|
110
113
|
if (pending.kind === "check")
|
|
111
114
|
return {
|
|
112
|
-
title: `Check ${pending.domains.length
|
|
115
|
+
title: `Check ${plural(pending.domains.length, "domain")}`,
|
|
113
116
|
description: "Looks up availability and price. The API documents no charge for this check.",
|
|
114
117
|
credits: 0,
|
|
115
118
|
label: "Check availability",
|
|
@@ -209,7 +212,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
209
212
|
/>
|
|
210
213
|
</div>
|
|
211
214
|
<Button disabled={buyable.length === 0 || !contactOk} onClick={() => setPending({ kind: "buy", domains: buyable })}>
|
|
212
|
-
Buy {buyable.length
|
|
215
|
+
Buy {plural(buyable.length, "domain")}
|
|
213
216
|
</Button>
|
|
214
217
|
</section>
|
|
215
218
|
|
|
@@ -226,7 +229,7 @@ export function DomainPurchase({ onPurchased, className }: DomainPurchaseProps)
|
|
|
226
229
|
) : domains.loading && !domains.data ? (
|
|
227
230
|
<Skeleton className="h-16 w-full" />
|
|
228
231
|
) : rows.length === 0 ? (
|
|
229
|
-
<EmptyState title="No sending domains yet" description=
|
|
232
|
+
<EmptyState title="No sending domains yet" description={`Buy one above, or add a domain you already own in the ${brand.linkLabel} app.`} />
|
|
230
233
|
) : (
|
|
231
234
|
<ul className="divide-y rounded border border-gray-200">
|
|
232
235
|
{rows.map((d) => (
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MailboxDetail: one mailbox, complete. Every field the API returns, the domain it sits on, its warmup and
|
|
5
|
+
* deliverability, and every action: test the connection, archive or restore, delete.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getMailbox GET /mailboxes/{mailbox_id} (read; all fields)
|
|
9
|
+
* 2. listProviderDomains GET /provider-domains (read; finds the domain this address sits on)
|
|
10
|
+
* 3. testMailboxConnection POST /mailboxes/{mailbox_id}/test-connection (write; SMTP and IMAP, no spend)
|
|
11
|
+
* 4. archiveMailbox POST /mailboxes/{mailbox_id}/archive (destructive: confirmed; `is_archived: false` restores)
|
|
12
|
+
* 5. deleteMailbox DELETE /mailboxes/{mailbox_id} (destructive: confirmed)
|
|
13
|
+
* Warmup tab: WarmupHealth (analytics, start/stop warmup, confirmed placement test).
|
|
14
|
+
*
|
|
15
|
+
* Tiers: 4 and 5 are destructive, so each opens ConfirmSpendDialog. Credentials are write-only and never shown.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import type { ReactNode } from "react";
|
|
20
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
24
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
25
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
26
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
27
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
28
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
29
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
31
|
+
import { fieldItems, listOf, objectOf } from "../lib/mailboxes-utils";
|
|
32
|
+
import type { DomainRow, MailboxRow } from "../lib/mailboxes-utils";
|
|
33
|
+
import { WarmupHealth } from "./warmup-health";
|
|
34
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
35
|
+
|
|
36
|
+
export interface MailboxDetailProps {
|
|
37
|
+
mailboxId: string | number;
|
|
38
|
+
/** Controlled tab ("details" | "warmup" | an extra tab id) and its change handler, for deep links. */
|
|
39
|
+
tab?: string;
|
|
40
|
+
onTabChange?: (tab: string) => void;
|
|
41
|
+
onClose?: () => void;
|
|
42
|
+
/** Full-page mode: show an "Open full page" (onExpand) or "Back to split view" (onCollapse) button. */
|
|
43
|
+
onExpand?: () => void;
|
|
44
|
+
onCollapse?: () => void;
|
|
45
|
+
/** Called after archive, restore or delete so a list can refresh. */
|
|
46
|
+
onChanged?: () => void;
|
|
47
|
+
/** Open the domain this mailbox sits on. */
|
|
48
|
+
onOpenDomain?: (domain: DomainRow) => void;
|
|
49
|
+
renderActions?: (mailbox: MailboxRow) => ReactNode;
|
|
50
|
+
extraTabs?: { id: string; label: string; render: (mailbox: MailboxRow) => ReactNode }[];
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Pending = "archive" | "restore" | "delete";
|
|
55
|
+
interface TestResult { success?: boolean; smtp?: { success?: boolean; error?: string }; imap?: { success?: boolean; error?: string } }
|
|
56
|
+
|
|
57
|
+
export function MailboxDetail({ mailboxId, tab, onTabChange, onClose, onExpand, onCollapse, onChanged, onOpenDomain, renderActions, extraTabs, className }: MailboxDetailProps) {
|
|
58
|
+
const brand = useBrand();
|
|
59
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
60
|
+
const [testNote, setTestNote] = useState<string | null>(null);
|
|
61
|
+
const q = useG8Query((signal) => g8Get(`/mailboxes/${mailboxId}`, undefined, { signal }), [mailboxId]);
|
|
62
|
+
const domains = useG8Query((signal) => g8Get("/provider-domains", { page: 1, limit: 100 }, { signal }), []);
|
|
63
|
+
|
|
64
|
+
const test = useG8Mutation(async () => {
|
|
65
|
+
const r = objectOf(await g8Post(`/mailboxes/${mailboxId}/test-connection`, {})) as TestResult;
|
|
66
|
+
setTestNote(
|
|
67
|
+
r.success
|
|
68
|
+
? "SMTP and IMAP both work"
|
|
69
|
+
: [r.smtp?.success === false && `SMTP: ${r.smtp.error ?? "failed"}`, r.imap?.success === false && `IMAP: ${r.imap.error ?? "failed"}`].filter(Boolean).join("; ") || "Connection failed",
|
|
70
|
+
);
|
|
71
|
+
await q.refetch();
|
|
72
|
+
});
|
|
73
|
+
const act = useG8Mutation(async (p: Pending) => {
|
|
74
|
+
if (p === "delete") await g8Delete(`/mailboxes/${mailboxId}`);
|
|
75
|
+
else await g8Post(`/mailboxes/${mailboxId}/archive`, { is_archived: p === "archive" });
|
|
76
|
+
setPending(null);
|
|
77
|
+
if (p === "delete") onClose?.();
|
|
78
|
+
else await q.refetch();
|
|
79
|
+
onChanged?.();
|
|
80
|
+
}, { onError: () => setPending(null) });
|
|
81
|
+
|
|
82
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the mailbox" onRetry={() => void q.refetch()} className={className} />;
|
|
83
|
+
if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
84
|
+
const raw = objectOf(q.data);
|
|
85
|
+
const m = { id: String(mailboxId), email: null, ...raw } as MailboxRow;
|
|
86
|
+
const domain = (m.email ?? "").split("@")[1]?.toLowerCase();
|
|
87
|
+
const domainRow = listOf<DomainRow>(domains.data).find((d) => d.domain?.toLowerCase() === domain);
|
|
88
|
+
const status = m.connection_status ?? "unknown";
|
|
89
|
+
const name = m.email ?? `Mailbox ${mailboxId}`;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<DetailPanel
|
|
93
|
+
className={className}
|
|
94
|
+
title={name}
|
|
95
|
+
subtitle={[m.display_name, m.provider].filter(Boolean).join(" - ") || undefined}
|
|
96
|
+
avatar={<EntityAvatar name={m.display_name || name} size="lg" />}
|
|
97
|
+
badges={
|
|
98
|
+
<>
|
|
99
|
+
<StatusPill tone={status === "active" ? "positive" : status === "unknown" ? "neutral" : "warning"} dot>{status}</StatusPill>
|
|
100
|
+
{m.is_archived ? <StatusPill tone="neutral">archived</StatusPill> : null}
|
|
101
|
+
</>
|
|
102
|
+
}
|
|
103
|
+
actions={
|
|
104
|
+
<>
|
|
105
|
+
<Button size="sm" variant="outline" loading={test.loading} onClick={() => void test.mutate(undefined)}>Test connection</Button>
|
|
106
|
+
<Button size="sm" variant="outline" onClick={() => setPending(m.is_archived ? "restore" : "archive")}>{m.is_archived ? "Restore mailbox" : "Archive mailbox"}</Button>
|
|
107
|
+
<Button size="sm" variant="outline" onClick={() => setPending("delete")}>Delete mailbox</Button>
|
|
108
|
+
{renderActions?.(m)}
|
|
109
|
+
<ConfirmSpendDialog
|
|
110
|
+
open={pending !== null}
|
|
111
|
+
tier="destructive"
|
|
112
|
+
title={`${pending === "delete" ? "Delete" : pending === "restore" ? "Restore" : "Archive"} ${name}`}
|
|
113
|
+
description={
|
|
114
|
+
pending === "delete" ? "Sequences that send from it can no longer send. This cannot be undone."
|
|
115
|
+
: pending === "restore" ? "It comes back into pickers. Nothing is deleted."
|
|
116
|
+
: "It disappears from pickers. History is kept and you can restore it."
|
|
117
|
+
}
|
|
118
|
+
confirmLabel={pending === "delete" ? "Delete mailbox" : pending === "restore" ? "Restore mailbox" : "Archive mailbox"}
|
|
119
|
+
affected={1}
|
|
120
|
+
confirming={act.loading}
|
|
121
|
+
onCancel={() => setPending(null)}
|
|
122
|
+
onConfirm={() => pending && void act.mutate(pending)}
|
|
123
|
+
/>
|
|
124
|
+
</>
|
|
125
|
+
}
|
|
126
|
+
summary={testNote ? <p role="status" className="px-5 py-2 text-sm">{testNote}</p> : undefined}
|
|
127
|
+
onClose={onClose}
|
|
128
|
+
onExpand={onExpand}
|
|
129
|
+
onCollapse={onCollapse}
|
|
130
|
+
activeTab={tab}
|
|
131
|
+
onTabChange={onTabChange}
|
|
132
|
+
tabs={[
|
|
133
|
+
{
|
|
134
|
+
id: "details",
|
|
135
|
+
label: "Details",
|
|
136
|
+
content: (
|
|
137
|
+
<div className="space-y-4">
|
|
138
|
+
{test.error ? <ApiError error={test.error} onRetry={test.reset} /> : null}
|
|
139
|
+
{act.error ? <ApiError error={act.error} onRetry={act.reset} /> : null}
|
|
140
|
+
<KeyValueList columns={2} items={fieldItems(raw)} />
|
|
141
|
+
<SectionCard title="Domain" description="The sending domain this address belongs to.">
|
|
142
|
+
{domainRow ? (
|
|
143
|
+
<Button size="sm" variant="outline" disabled={!onOpenDomain} onClick={() => onOpenDomain?.(domainRow)}>{domainRow.domain}</Button>
|
|
144
|
+
) : (
|
|
145
|
+
<p className="text-sm text-muted-foreground">{domain ? `${domain} is not a domain bought or connected through ${brand.linkLabel}.` : "No domain."}</p>
|
|
146
|
+
)}
|
|
147
|
+
</SectionCard>
|
|
148
|
+
</div>
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
{ id: "warmup", label: "Warmup and health", content: <WarmupHealth mailboxId={mailboxId} showFleet={false} /> },
|
|
152
|
+
...(extraTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(m) })),
|
|
153
|
+
]}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MailboxesScreen: the finished "Mailboxes" settings page. Everything about the inboxes you send from.
|
|
5
|
+
*
|
|
6
|
+
* Composes: MailboxList, MailboxDetail (every field, warmup, test, archive, restore, delete), DomainDetail, WarmupHealth (fleet verdict, per-mailbox warmup and
|
|
7
|
+
* placement test), ConnectMailbox (IMAP/SMTP, Google/Microsoft link-out, buy mailboxes), DomainPurchase
|
|
8
|
+
* (suggest, check, buy, connect, DNS).
|
|
9
|
+
*
|
|
10
|
+
* User flows:
|
|
11
|
+
* - See fleet health at a glance (stat strip from the deliverability summary).
|
|
12
|
+
* - Every record opens a complete detail (MailboxDetail, DomainDetail) beside the list or as a full page
|
|
13
|
+
* (`?view=full`), deep-linked with `?id=`, `?tab=`, `?dtab=`.
|
|
14
|
+
* - Mailboxes tab: browse mailboxes, show archived (an archived one can be restored, confirmed), test a connection, archive or delete (confirmed),
|
|
15
|
+
* open one (deep link `?id=`) for warmup analytics, start/stop warmup and a confirmed placement test.
|
|
16
|
+
* - "Add mailbox" opens a sheet: connect over IMAP/SMTP, open the graph8 app for Google/Microsoft, or buy
|
|
17
|
+
* mailboxes on a connected domain (confirmed with a credit estimate).
|
|
18
|
+
* - Deliverability tab: the safe-to-send verdict, fleet counts and degraded mailboxes.
|
|
19
|
+
* - Domains tab: suggest, check availability, buy (confirmed), connect and check DNS propagation.
|
|
20
|
+
*
|
|
21
|
+
* SDK operations: none of its own, plus one read for the stat strip:
|
|
22
|
+
* getFleetSummary GET /deliverability/fleet-summary (read)
|
|
23
|
+
* Billable and destructive calls live in the composed parts and open ConfirmSpendDialog there.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { useState } from "react";
|
|
27
|
+
import type { ReactNode } from "react";
|
|
28
|
+
import { Mail, Plus } from "lucide-react";
|
|
29
|
+
import { Button } from "@graph8/sdk/ui";
|
|
30
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
31
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
32
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
33
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
34
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
35
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
36
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
37
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
38
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
39
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
40
|
+
import { listOf, objectOf } from "../lib/mailboxes-utils";
|
|
41
|
+
import type { DomainRow, MailboxRow } from "../lib/mailboxes-utils";
|
|
42
|
+
import { ConnectMailbox } from "./connect-mailbox";
|
|
43
|
+
import { DomainPurchase } from "./domain-purchase";
|
|
44
|
+
import { DomainDetail } from "./domain-detail";
|
|
45
|
+
import { MailboxDetail } from "./mailbox-detail";
|
|
46
|
+
import { MailboxList } from "./mailbox-list";
|
|
47
|
+
import { WarmupHealth } from "./warmup-health";
|
|
48
|
+
|
|
49
|
+
export interface MailboxesScreenProps {
|
|
50
|
+
/** Where the person connects Google or Microsoft. Default: graph8 Settings > Purchase > Mailboxes. */
|
|
51
|
+
appUrl?: string;
|
|
52
|
+
/** Extra buttons in the toolbar. */
|
|
53
|
+
toolbarActions?: ReactNode;
|
|
54
|
+
/** Extra buttons in the open mailbox's header. */
|
|
55
|
+
renderDetailActions?: (mailbox: MailboxRow) => ReactNode;
|
|
56
|
+
/** Extra tabs in the open mailbox's panel. */
|
|
57
|
+
extraDetailTabs?: { id: string; label: string; render: (mailbox: MailboxRow) => ReactNode }[];
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const TABS = [
|
|
62
|
+
{ id: "mailboxes", label: "Mailboxes" },
|
|
63
|
+
{ id: "deliverability", label: "Deliverability" },
|
|
64
|
+
{ id: "domains", label: "Domains" },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const STAT_ORDER = ["total", "healthy", "watch", "paused", "inbox_rate"];
|
|
68
|
+
|
|
69
|
+
function label(key: string): string {
|
|
70
|
+
const s = key.replace(/_/g, " ");
|
|
71
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Up to four numeric fleet fields; a 0..1 rate reads as a percent. Unknown shapes just show what is there. */
|
|
75
|
+
export function fleetStats(summary: Record<string, unknown>): StatItem[] {
|
|
76
|
+
const nums = Object.entries(summary).filter(([, v]) => typeof v === "number") as Array<[string, number]>;
|
|
77
|
+
nums.sort((a, b) => (STAT_ORDER.indexOf(a[0]) + 1 || 99) - (STAT_ORDER.indexOf(b[0]) + 1 || 99));
|
|
78
|
+
return nums.slice(0, 4).map(([k, v]) => ({
|
|
79
|
+
label: label(k),
|
|
80
|
+
value: k.includes("rate") && v <= 1 ? `${Math.round(v * 100)}%` : v.toLocaleString(),
|
|
81
|
+
tone: k === "healthy" ? "positive" : k === "watch" ? "warning" : undefined,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function DomainList({ version, selectedId, onOpen }: { version: number; selectedId: string; onOpen: (d: DomainRow) => void }) {
|
|
86
|
+
const q = useG8Query((signal) => g8Get("/provider-domains", { page: 1, limit: 100 }, { signal }), [version]);
|
|
87
|
+
const rows = listOf<DomainRow>(q.data);
|
|
88
|
+
if (q.loading && !q.data) return <Skeleton className="h-24 w-full" />;
|
|
89
|
+
if (rows.length === 0) return null;
|
|
90
|
+
return (
|
|
91
|
+
<ul aria-label="Your domains" className="divide-y rounded-md border">
|
|
92
|
+
{rows.map((d) => (
|
|
93
|
+
<li key={d.id} className={`flex items-center justify-between px-3 py-2 text-sm ${String(d.id) === selectedId ? "bg-muted" : ""}`}>
|
|
94
|
+
<button type="button" className="font-medium hover:underline" onClick={() => onOpen(d)}>{d.domain}</button>
|
|
95
|
+
<span className="flex items-center gap-2">
|
|
96
|
+
{d.mailbox_count != null ? <span className="text-xs text-muted-foreground">{d.mailbox_count} mailboxes</span> : null}
|
|
97
|
+
<StatusPill tone={d.provider_connected ? "positive" : "warning"} dot>{d.provider_connected ? "Connected" : "Not connected"}</StatusPill>
|
|
98
|
+
</span>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function MailboxesScreen({ appUrl, toolbarActions, renderDetailActions, extraDetailTabs, className }: MailboxesScreenProps) {
|
|
106
|
+
const [tab, setTab] = useUrlState("tab", "mailboxes");
|
|
107
|
+
const [selectedId, setSelectedId] = useUrlState("id", "");
|
|
108
|
+
const [detailTab, setDetailTab] = useUrlState("dtab", "");
|
|
109
|
+
const [view, setView] = useUrlState("view", "");
|
|
110
|
+
const [archived, setArchived] = useState(false);
|
|
111
|
+
const [adding, setAdding] = useState<{ domainId?: number | string } | null>(null);
|
|
112
|
+
const [version, setVersion] = useState(0);
|
|
113
|
+
const summary = useG8Query((signal) => g8Get("/deliverability/fleet-summary", undefined, { signal }), [version]);
|
|
114
|
+
const stats = fleetStats(objectOf(summary.data));
|
|
115
|
+
const bump = () => setVersion((v) => v + 1);
|
|
116
|
+
|
|
117
|
+
const go = (nextTab: string, id: string) => {
|
|
118
|
+
setTab(nextTab === "mailboxes" ? null : nextTab);
|
|
119
|
+
setSelectedId(id || null);
|
|
120
|
+
setDetailTab(null);
|
|
121
|
+
setView(null);
|
|
122
|
+
};
|
|
123
|
+
const close = () => { setSelectedId(null); setDetailTab(null); setView(null); };
|
|
124
|
+
const add = (
|
|
125
|
+
<Button onClick={() => setAdding({})}>
|
|
126
|
+
<Plus className="mr-1 h-4 w-4" aria-hidden /> Add mailbox
|
|
127
|
+
</Button>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<ScreenLayout
|
|
132
|
+
className={className}
|
|
133
|
+
title="Mailboxes"
|
|
134
|
+
description="The inboxes you send from: connect, warm up and keep them healthy."
|
|
135
|
+
icon={<Mail className="h-5 w-5" aria-hidden />}
|
|
136
|
+
actions={add}
|
|
137
|
+
tabs={TABS}
|
|
138
|
+
activeTab={TABS.some((t) => t.id === tab) ? tab : "mailboxes"}
|
|
139
|
+
onTabChange={(id) => go(id, "")}
|
|
140
|
+
stats={stats.length > 0 ? stats : undefined}
|
|
141
|
+
>
|
|
142
|
+
{tab === "deliverability" ? (
|
|
143
|
+
<WarmupHealth />
|
|
144
|
+
) : tab === "domains" ? (
|
|
145
|
+
<SplitView
|
|
146
|
+
expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
147
|
+
detailOpen={Boolean(selectedId)}
|
|
148
|
+
onCloseDetail={close}
|
|
149
|
+
list={
|
|
150
|
+
<div className="space-y-4">
|
|
151
|
+
<DomainList version={version} selectedId={selectedId} onOpen={(d) => go("domains", String(d.id))} />
|
|
152
|
+
<DomainPurchase onPurchased={bump} />
|
|
153
|
+
</div>
|
|
154
|
+
}
|
|
155
|
+
detail={
|
|
156
|
+
selectedId ? (
|
|
157
|
+
<DomainDetail
|
|
158
|
+
key={selectedId}
|
|
159
|
+
domainId={selectedId}
|
|
160
|
+
onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined}
|
|
161
|
+
onClose={close}
|
|
162
|
+
onChanged={bump}
|
|
163
|
+
onBuyMailboxes={(d) => setAdding({ domainId: d.id })}
|
|
164
|
+
onOpenMailbox={(m) => go("mailboxes", m.id)}
|
|
165
|
+
/>
|
|
166
|
+
) : null
|
|
167
|
+
}
|
|
168
|
+
/>
|
|
169
|
+
) : (
|
|
170
|
+
<div className="space-y-3">
|
|
171
|
+
<Toolbar
|
|
172
|
+
actions={toolbarActions}
|
|
173
|
+
filters={
|
|
174
|
+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
175
|
+
<input type="checkbox" checked={archived} onChange={(e) => setArchived(e.target.checked)} />
|
|
176
|
+
Show archived
|
|
177
|
+
</label>
|
|
178
|
+
}
|
|
179
|
+
/>
|
|
180
|
+
<SplitView
|
|
181
|
+
expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
182
|
+
detailOpen={Boolean(selectedId)}
|
|
183
|
+
onCloseDetail={close}
|
|
184
|
+
list={<MailboxList key={`${version}:${archived}`} includeArchived={archived} onSelect={(m) => go("mailboxes", m.id)} emptyAction={add} />}
|
|
185
|
+
detail={
|
|
186
|
+
selectedId ? (
|
|
187
|
+
<MailboxDetail
|
|
188
|
+
key={selectedId}
|
|
189
|
+
mailboxId={selectedId}
|
|
190
|
+
onExpand={view === "full" ? undefined : () => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined}
|
|
191
|
+
tab={detailTab || undefined}
|
|
192
|
+
onTabChange={(t) => setDetailTab(t === "details" ? null : t)}
|
|
193
|
+
onClose={close}
|
|
194
|
+
onChanged={bump}
|
|
195
|
+
onOpenDomain={(d) => go("domains", String(d.id))}
|
|
196
|
+
renderActions={renderDetailActions}
|
|
197
|
+
extraTabs={extraDetailTabs}
|
|
198
|
+
/>
|
|
199
|
+
) : null
|
|
200
|
+
}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
<FormSheet
|
|
205
|
+
width="lg"
|
|
206
|
+
open={adding !== null}
|
|
207
|
+
onOpenChange={(o) => !o && setAdding(null)}
|
|
208
|
+
title={adding?.domainId != null ? "Buy mailboxes" : "Add a mailbox"}
|
|
209
|
+
description="Connect one you own, use Google or Microsoft, or buy new ones."
|
|
210
|
+
>
|
|
211
|
+
{adding ? (
|
|
212
|
+
<ConnectMailbox
|
|
213
|
+
appUrl={appUrl}
|
|
214
|
+
initialMode={adding.domainId != null ? "buy" : "imap"}
|
|
215
|
+
domainId={adding.domainId}
|
|
216
|
+
onConnected={() => { setAdding(null); bump(); }}
|
|
217
|
+
onPurchased={bump}
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
220
|
+
</FormSheet>
|
|
221
|
+
</ScreenLayout>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -29,6 +29,8 @@ import { listOf, objectOf, PLACEMENT_TEST_CREDITS, scalars, statusClass } from "
|
|
|
29
29
|
export interface WarmupHealthProps {
|
|
30
30
|
/** Show warmup analytics and controls for this mailbox (an id from MailboxList). */
|
|
31
31
|
mailboxId?: string | number;
|
|
32
|
+
/** Show the fleet verdict and counts above the mailbox section. Default true; screens that already show them pass false. */
|
|
33
|
+
showFleet?: boolean;
|
|
32
34
|
className?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -222,10 +224,10 @@ function MailboxWarmup({ mailboxId }: { mailboxId: string | number }) {
|
|
|
222
224
|
);
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
export function WarmupHealth({ mailboxId, className }: WarmupHealthProps) {
|
|
227
|
+
export function WarmupHealth({ mailboxId, showFleet = true, className }: WarmupHealthProps) {
|
|
226
228
|
return (
|
|
227
229
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
228
|
-
<Fleet />
|
|
230
|
+
{showFleet || mailboxId == null ? <Fleet /> : null}
|
|
229
231
|
{mailboxId != null ? <MailboxWarmup mailboxId={mailboxId} /> : null}
|
|
230
232
|
</div>
|
|
231
233
|
);
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
/** Types and helpers shared by the mailbox components. Open-shaped payloads: every field is optional. */
|
|
4
4
|
|
|
5
|
+
import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
|
|
6
|
+
|
|
5
7
|
/** Where the person connects a Google or Microsoft mailbox. OAuth consent can only run in the graph8 app. */
|
|
6
|
-
|
|
8
|
+
/** The path on the platform's web app; components resolve it against the brand (`brandUrl(useBrand(), ...)`). */
|
|
9
|
+
export const GRAPH8_APP_MAILBOXES_PATH = "/studio/settings?tab=purchase&view=mailboxes";
|
|
10
|
+
/** The default (graph8) URL. Kept for existing imports; prefer GRAPH8_APP_MAILBOXES_PATH with useBrand. */
|
|
11
|
+
export const GRAPH8_APP_MAILBOXES_URL = `${DEFAULT_BRAND.appUrl}${GRAPH8_APP_MAILBOXES_PATH}`;
|
|
7
12
|
|
|
8
13
|
/** The API holds these when you buy (docs/developer-api-reference.md, Channel Purchase); the real rate can change. */
|
|
9
14
|
export const DEFAULT_DOMAIN_CREDITS = 2500;
|
|
@@ -67,3 +72,19 @@ export function newIdempotencyKey(): string {
|
|
|
67
72
|
? crypto.randomUUID()
|
|
68
73
|
: `g8-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
69
74
|
}
|
|
75
|
+
|
|
76
|
+
const HIDDEN_FIELDS = /password|secret|token|credential/i;
|
|
77
|
+
|
|
78
|
+
/** Every readable field of an open-shaped record as label/value pairs (secrets are never shown). */
|
|
79
|
+
export function fieldItems(obj: Record<string, unknown>): Array<{ label: string; value: string }> {
|
|
80
|
+
return Object.entries(obj)
|
|
81
|
+
.filter(([k, v]) => !HIDDEN_FIELDS.test(k) && v !== null && v !== undefined && v !== "")
|
|
82
|
+
.map(([k, v]) => {
|
|
83
|
+
const label = k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
84
|
+
if (typeof v === "string" && /_at$|date$/.test(k) && !Number.isNaN(new Date(v).getTime()))
|
|
85
|
+
return { label, value: new Date(v).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short", timeZone: "UTC" }) + " UTC" };
|
|
86
|
+
if (typeof v === "boolean") return { label, value: v ? "Yes" : "No" };
|
|
87
|
+
if (typeof v === "object") return { label, value: JSON.stringify(v) };
|
|
88
|
+
return { label, value: String(v) };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -4,6 +4,18 @@ Write, test, schedule and send newsletter issues, manage subscribers, and read o
|
|
|
4
4
|
**Sending reaches real inboxes and cannot be recalled**, so every call that can mail a person is tier `external`
|
|
5
5
|
and goes through `ConfirmSpendDialog` with the recipient count.
|
|
6
6
|
|
|
7
|
+
## Screens
|
|
8
|
+
|
|
9
|
+
`NewsletterScreen` (`components/newsletter-screen.tsx`) is the finished page: mount it and a person can run a newsletter end to end, no extra code.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { NewsletterScreen } from "@/g8/newsletter/components/newsletter-screen";
|
|
13
|
+
export default function Page() { return <NewsletterScreen />; }
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Flows: pick or create a newsletter (deep link `?newsletter=`), activate or pause it, read subscribers and open and click rates, search and filter issues, write an issue (deep link `?id=`), generate it with AI, send a test, send now or schedule, follow the send and pause, resume or cancel it, read per-issue results, manage subscribers (add, import a list, verify, unsubscribe, erase), compare sent issues. Every send, import, verify, unsubscribe and erase is confirmed.
|
|
17
|
+
Prerequisites are the ones below (verified sending domain, consent, `campaigns:launch`). Optional props: `renderRowActions`, `renderDetailActions`, `extraDetailTabs`, `toolbarActions`.
|
|
18
|
+
|
|
7
19
|
## Install
|
|
8
20
|
|
|
9
21
|
```
|
|
@@ -82,6 +94,7 @@ export default function Page() {
|
|
|
82
94
|
|
|
83
95
|
| Component | File | What it is |
|
|
84
96
|
|---|---|---|
|
|
97
|
+
| `NewsletterScreen` | `components/newsletter-screen.tsx` | The finished page: issues with write, send and results, subscribers and performance. |
|
|
85
98
|
| `NewsletterEditor` | `components/newsletter-editor.tsx` | Pick or create a newsletter, list its issues, write issues and activate or pause the newsletter. |
|
|
86
99
|
| `IssueForm` | `components/issue-form.tsx` | Draft one issue: subject, preview text, HTML, AI generation, test send and delete of drafts. |
|
|
87
100
|
| `IssueScheduler` | `components/issue-scheduler.tsx` | Send an issue now or schedule it, confirmed with the subscriber count, then follow, pause, resume or cancel the send. |
|