@graph8/sdk 0.370.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -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/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/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- 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 +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/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 +6 -0
- package/blocks/crm/block.json +53 -4
- 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-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- 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/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -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/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +2 -2
- 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 +4 -1
- 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/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -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 +51 -17
- 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 +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- 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 +36 -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 +113 -1
- 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/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -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 +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- 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 +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- 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/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- 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/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -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 +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +36 -8
- 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 +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -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 +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- 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 +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- 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 +8 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +20 -5
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +27 -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 +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -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 +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -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 +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- 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
|
@@ -18,28 +18,51 @@
|
|
|
18
18
|
"activate_newsletter_newsletters__newsletter_id__activate_post",
|
|
19
19
|
"add_subscriber_newsletters__newsletter_id__subscribers_post",
|
|
20
20
|
"cancel_send_newsletters__newsletter_id__issues__issue_id__send_cancel_post",
|
|
21
|
+
"complete_newsletter_newsletters__newsletter_id__complete_post",
|
|
21
22
|
"create_issue_newsletters__newsletter_id__issues_post",
|
|
22
23
|
"create_newsletter_newsletters_post",
|
|
24
|
+
"create_template_from_prompt_newsletters_templates_custom_post",
|
|
23
25
|
"delete_issue_newsletters__newsletter_id__issues__issue_id__delete",
|
|
26
|
+
"delete_newsletter_template_newsletters_templates__template_id__delete",
|
|
24
27
|
"delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
|
|
28
|
+
"disable_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__delete",
|
|
29
|
+
"duplicate_issue_newsletters__newsletter_id__issues__issue_id__duplicate_post",
|
|
30
|
+
"enable_newsletter_list_sync_newsletters__newsletter_id__list_syncs_post",
|
|
25
31
|
"generate_newsletter_issue_newsletters__newsletter_id__issues__issue_id__generate_post",
|
|
26
32
|
"get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get",
|
|
27
33
|
"get_issue_generation_status_newsletters__newsletter_id__issues__issue_id__generation_status_get",
|
|
28
34
|
"get_issue_newsletters__newsletter_id__issues__issue_id__get",
|
|
35
|
+
"get_newsletter_newsletters__newsletter_id__get",
|
|
36
|
+
"get_newsletter_settings_newsletters__newsletter_id__settings_get",
|
|
29
37
|
"get_send_status_newsletters__newsletter_id__issues__issue_id__send_get",
|
|
38
|
+
"get_subscriber_timeline_newsletters__newsletter_id__issues__issue_id__analytics_subscribers__subscriber_id__timeline_get",
|
|
30
39
|
"import_subscribers_from_audience_newsletters__newsletter_id__subscribers_import_audience_post",
|
|
31
40
|
"list_issues_newsletters__newsletter_id__issues_get",
|
|
32
41
|
"list_lists_lists_get",
|
|
42
|
+
"list_newsletter_issue_versions_newsletters__newsletter_id__issues__issue_id__versions_get",
|
|
43
|
+
"list_newsletter_list_syncs_newsletters__newsletter_id__list_syncs_get",
|
|
33
44
|
"list_newsletters_newsletters_get",
|
|
34
45
|
"list_subscribers_newsletters__newsletter_id__subscribers_get",
|
|
46
|
+
"list_templates_newsletters_templates_get",
|
|
47
|
+
"newsletter_domain_status_newsletters__newsletter_id__settings_domain_status_get",
|
|
35
48
|
"pause_newsletter_newsletters__newsletter_id__pause_post",
|
|
36
49
|
"pause_send_newsletters__newsletter_id__issues__issue_id__send_pause_post",
|
|
37
50
|
"preview_newsletter_subscriber_verification_newsletters__newsletter_id__subscribers_verify_preview_post",
|
|
51
|
+
"restore_newsletter_issue_version_newsletters__newsletter_id__issues__issue_id__versions__version_num__restore_post",
|
|
52
|
+
"resume_newsletter_newsletters__newsletter_id__resume_post",
|
|
38
53
|
"resume_send_newsletters__newsletter_id__issues__issue_id__send_resume_post",
|
|
54
|
+
"run_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__run_post",
|
|
39
55
|
"send_issue_newsletters__newsletter_id__issues__issue_id__send_post",
|
|
40
56
|
"send_test_email_newsletters__newsletter_id__issues__issue_id__send_test_post",
|
|
57
|
+
"set_template_approval_newsletters_templates__template_id__approval_post",
|
|
58
|
+
"template_generation_status_newsletters_templates__template_id__generation_status_get",
|
|
41
59
|
"unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch",
|
|
42
60
|
"update_issue_newsletters__newsletter_id__issues__issue_id__put",
|
|
61
|
+
"update_newsletter_newsletters__newsletter_id__patch",
|
|
62
|
+
"update_newsletter_settings_newsletters__newsletter_id__settings_put",
|
|
63
|
+
"update_newsletter_template_newsletters_templates__template_id__patch",
|
|
64
|
+
"use_template_newsletters_templates__template_id__use_post",
|
|
65
|
+
"verify_newsletter_domain_newsletters__newsletter_id__settings_verify_domain_post",
|
|
43
66
|
"verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post"
|
|
44
67
|
],
|
|
45
68
|
"components": [
|
|
@@ -155,6 +178,96 @@
|
|
|
155
178
|
"tiers": [
|
|
156
179
|
"read"
|
|
157
180
|
]
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "IssueVersions",
|
|
184
|
+
"file": "components/issue-versions.tsx",
|
|
185
|
+
"purpose": "Version history of one issue with restore, and duplicate into a new draft.",
|
|
186
|
+
"operations": [
|
|
187
|
+
"list_newsletter_issue_versions_newsletters__newsletter_id__issues__issue_id__versions_get",
|
|
188
|
+
"restore_newsletter_issue_version_newsletters__newsletter_id__issues__issue_id__versions__version_num__restore_post",
|
|
189
|
+
"duplicate_issue_newsletters__newsletter_id__issues__issue_id__duplicate_post"
|
|
190
|
+
],
|
|
191
|
+
"tiers": [
|
|
192
|
+
"read",
|
|
193
|
+
"write",
|
|
194
|
+
"external"
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "MoreRows",
|
|
199
|
+
"file": "components/more-rows.tsx",
|
|
200
|
+
"purpose": "Footer for paged lists: how many rows are shown and a Load more button.",
|
|
201
|
+
"operations": [],
|
|
202
|
+
"tiers": []
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"name": "NewsletterListSyncs",
|
|
206
|
+
"file": "components/newsletter-list-syncs.tsx",
|
|
207
|
+
"purpose": "Keep subscribers in step with lists: turn a sync on (consent confirmed), run it, turn it off.",
|
|
208
|
+
"operations": [
|
|
209
|
+
"list_newsletter_list_syncs_newsletters__newsletter_id__list_syncs_get",
|
|
210
|
+
"list_lists_lists_get",
|
|
211
|
+
"enable_newsletter_list_sync_newsletters__newsletter_id__list_syncs_post",
|
|
212
|
+
"run_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__run_post",
|
|
213
|
+
"disable_newsletter_list_sync_newsletters__newsletter_id__list_syncs__audience_list_id__delete"
|
|
214
|
+
],
|
|
215
|
+
"tiers": [
|
|
216
|
+
"read",
|
|
217
|
+
"write",
|
|
218
|
+
"destructive"
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"name": "NewsletterSettings",
|
|
223
|
+
"file": "components/newsletter-settings.tsx",
|
|
224
|
+
"purpose": "The newsletter itself: name and goal, sender and opt-in, sending domain verification, resume and complete.",
|
|
225
|
+
"operations": [
|
|
226
|
+
"update_newsletter_newsletters__newsletter_id__patch",
|
|
227
|
+
"update_newsletter_settings_newsletters__newsletter_id__settings_put",
|
|
228
|
+
"newsletter_domain_status_newsletters__newsletter_id__settings_domain_status_get",
|
|
229
|
+
"verify_newsletter_domain_newsletters__newsletter_id__settings_verify_domain_post",
|
|
230
|
+
"complete_newsletter_newsletters__newsletter_id__complete_post",
|
|
231
|
+
"resume_newsletter_newsletters__newsletter_id__resume_post",
|
|
232
|
+
"get_newsletter_settings_newsletters__newsletter_id__settings_get",
|
|
233
|
+
"get_newsletter_newsletters__newsletter_id__get"
|
|
234
|
+
],
|
|
235
|
+
"tiers": [
|
|
236
|
+
"read",
|
|
237
|
+
"write",
|
|
238
|
+
"external"
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"name": "NewsletterTemplates",
|
|
243
|
+
"file": "components/newsletter-templates.tsx",
|
|
244
|
+
"purpose": "Saved issue templates: start a draft from one, approve, rename, delete, or write one with AI.",
|
|
245
|
+
"operations": [
|
|
246
|
+
"update_newsletter_template_newsletters_templates__template_id__patch",
|
|
247
|
+
"set_template_approval_newsletters_templates__template_id__approval_post",
|
|
248
|
+
"use_template_newsletters_templates__template_id__use_post",
|
|
249
|
+
"template_generation_status_newsletters_templates__template_id__generation_status_get",
|
|
250
|
+
"delete_newsletter_template_newsletters_templates__template_id__delete",
|
|
251
|
+
"list_templates_newsletters_templates_get",
|
|
252
|
+
"create_template_from_prompt_newsletters_templates_custom_post"
|
|
253
|
+
],
|
|
254
|
+
"tiers": [
|
|
255
|
+
"read",
|
|
256
|
+
"write",
|
|
257
|
+
"billable",
|
|
258
|
+
"destructive"
|
|
259
|
+
]
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"name": "SubscriberTimeline",
|
|
263
|
+
"file": "components/subscriber-timeline.tsx",
|
|
264
|
+
"purpose": "What one subscriber did with one sent issue.",
|
|
265
|
+
"operations": [
|
|
266
|
+
"get_subscriber_timeline_newsletters__newsletter_id__issues__issue_id__analytics_subscribers__subscriber_id__timeline_get"
|
|
267
|
+
],
|
|
268
|
+
"tiers": [
|
|
269
|
+
"read"
|
|
270
|
+
]
|
|
158
271
|
}
|
|
159
272
|
],
|
|
160
273
|
"hooks": [],
|
|
@@ -162,6 +275,10 @@
|
|
|
162
275
|
{
|
|
163
276
|
"file": "lib/newsletter-types.ts",
|
|
164
277
|
"purpose": "Newsletter/issue/subscriber types and helpers: draft check, percentages, scheduled-time conversion, generation-in-progress guard."
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"file": "lib/newsletter-extra.ts",
|
|
281
|
+
"purpose": "Helpers for the newsletter management components: open-dict readers, changed-fields diff, domain and DNS helpers."
|
|
165
282
|
}
|
|
166
283
|
],
|
|
167
284
|
"recipes": [
|
|
@@ -109,7 +109,7 @@ export function IssueScheduler({ newsletterId, issueId, onSent }: IssueScheduler
|
|
|
109
109
|
<Card>
|
|
110
110
|
<CardHeader><CardTitle>{issueTitle(issue.data)} <span className="text-muted-foreground text-sm font-normal">{issue.data.status}</span></CardTitle></CardHeader>
|
|
111
111
|
<CardContent className="grid gap-4">
|
|
112
|
-
{partial ? <p role="alert" className="text-sm text-
|
|
112
|
+
{partial ? <p role="alert" className="text-sm text-warning-foreground">This issue is still being written by AI. Wait for it to finish: sending now would mail the partial version.</p> : null}
|
|
113
113
|
{draft ? (
|
|
114
114
|
<div className="grid gap-3">
|
|
115
115
|
<p className="text-sm">Audience: <strong>{audience.loading ? "..." : subscribers.toLocaleString()}</strong> subscribed {pluralWord(subscribers, "person", "people")}.</p>
|
|
@@ -118,7 +118,7 @@ export function IssueScheduler({ newsletterId, issueId, onSent }: IssueScheduler
|
|
|
118
118
|
<Button size="sm" variant={when === "later" ? "secondary" : "ghost"} onClick={() => setWhen("later")}>Schedule</Button>
|
|
119
119
|
</div>
|
|
120
120
|
{when === "later" ? <div className="max-w-xs"><Label htmlFor="g8-send-at" className="mb-1">Send at</Label><Input id="g8-send-at" type="datetime-local" value={at} onChange={(e) => setAt(e.target.value)} /></div> : null}
|
|
121
|
-
{inPast ? <p role="alert" className="text-sm text-
|
|
121
|
+
{inPast ? <p role="alert" className="text-sm text-error-foreground">Pick a time in the future.</p> : null}
|
|
122
122
|
<div><Button disabled={blocked} onClick={() => void openSend()}>{when === "later" ? "Schedule issue" : "Send issue"}</Button></div>
|
|
123
123
|
{subscribers === 0 && !audience.loading ? <p className="text-muted-foreground text-sm">Add subscribers first (SubscriberTable).</p> : null}
|
|
124
124
|
</div>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IssueVersions: the saved versions of one issue, newest first, with restore, and a Duplicate action that copies the
|
|
5
|
+
* issue into a new draft.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. listNewsletterIssueVersions GET /newsletters/{id}/issues/{issue}/versions (read)
|
|
9
|
+
* 2. restoreNewsletterIssueVersion POST /newsletters/{id}/issues/{issue}/versions/{n}/restore (write; replaces the draft's content)
|
|
10
|
+
* 3. duplicateIssue POST /newsletters/{id}/issues/{issue}/duplicate (EXTERNAL, confirmed; creates a draft, sends nothing)
|
|
11
|
+
*
|
|
12
|
+
* Only a draft can be restored (a sent issue is a record of what people received). Restoring replaces the draft's
|
|
13
|
+
* current content, so the button says so and asks for a second click before it writes.
|
|
14
|
+
*/
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Copy } from "lucide-react";
|
|
17
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
26
|
+
import { isDraft } from "../lib/newsletter-types";
|
|
27
|
+
import type { Issue } from "../lib/newsletter-types";
|
|
28
|
+
import { asRec, num, rowsOf, str, versionNumber } from "../lib/newsletter-extra";
|
|
29
|
+
import type { Rec } from "../lib/newsletter-extra";
|
|
30
|
+
|
|
31
|
+
export interface IssueVersionsProps {
|
|
32
|
+
newsletterId: string;
|
|
33
|
+
issue: Pick<Issue, "id" | "status" | "subject">;
|
|
34
|
+
/** Called after a version was restored, so the editor reloads the draft. */
|
|
35
|
+
onRestored?: () => void;
|
|
36
|
+
/** Called with the new draft's id after Duplicate. */
|
|
37
|
+
onDuplicated?: (issueId: number) => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const when = (v: unknown): string => {
|
|
42
|
+
const d = new Date(str(v));
|
|
43
|
+
return Number.isNaN(d.getTime()) ? "" : d.toLocaleString();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function IssueVersions({ newsletterId, issue, onRestored, onDuplicated, className }: IssueVersionsProps) {
|
|
47
|
+
const base = `/newsletters/${encodeURIComponent(newsletterId)}/issues/${issue.id}`;
|
|
48
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
49
|
+
const versions = useG8Query((signal) => g8Get(`${base}/versions`, undefined, { signal }), [base]);
|
|
50
|
+
const [sure, setSure] = useState<number | null>(null);
|
|
51
|
+
const restore = useG8Mutation((n: number) => g8Post(`${base}/versions/${n}/restore`), { onSuccess: () => { setSure(null); void versions.refetch(); onRestored?.(); } });
|
|
52
|
+
const rows = rowsOf<Rec>(versions.data?.data, "versions");
|
|
53
|
+
const draft = isDraft(issue);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={className}>
|
|
57
|
+
<div className="grid gap-4">
|
|
58
|
+
<SectionCard title="Duplicate" description="Copy this issue into a new draft you can change and send separately."
|
|
59
|
+
actions={<Button size="sm" variant="outline" onClick={() => ask({
|
|
60
|
+
tier: "external", title: `Duplicate "${issue.subject || `issue ${issue.id}`}"`, confirmLabel: "Duplicate",
|
|
61
|
+
description: "Creates a new draft with the same content. Nothing is sent to anyone.",
|
|
62
|
+
run: async () => { const id = num(asRec((await g8Post(`${base}/duplicate`)).data).id); if (id !== undefined) onDuplicated?.(id); },
|
|
63
|
+
})}><Copy className="size-4" />Duplicate issue</Button>}>
|
|
64
|
+
<p className="text-muted-foreground text-sm">The copy starts as a draft in this newsletter.</p>
|
|
65
|
+
</SectionCard>
|
|
66
|
+
<SectionCard title="Version history" description={draft ? "Restoring replaces the draft's current content." : "A sent issue cannot be changed; its versions are shown for reference."}>
|
|
67
|
+
{versions.error ? <ApiError error={versions.error} title="Could not load the versions" onRetry={() => void versions.refetch()} /> : versions.loading && !versions.data ? <Skeleton className="h-20 w-full" aria-busy="true" /> : rows.length === 0 ? (
|
|
68
|
+
<EmptyState compact title="No saved versions yet" description="A version is saved each time the issue is saved or rewritten." />
|
|
69
|
+
) : (
|
|
70
|
+
<ul className="grid gap-2">
|
|
71
|
+
{rows.map((v, i) => {
|
|
72
|
+
const n = versionNumber(v);
|
|
73
|
+
return (
|
|
74
|
+
<li key={n ?? i} className="flex items-center justify-between gap-3 rounded-md border p-3">
|
|
75
|
+
<div className="min-w-0 text-sm"><span className="font-medium">Version {n ?? i + 1}</span>{str(v.source) ? <span className="text-muted-foreground"> from {str(v.source).replace(/_/g, " ")}</span> : null}<span className="text-muted-foreground block text-xs">{when(v.created_at)}{str(v.subject) ? ` · ${str(v.subject)}` : ""}</span></div>
|
|
76
|
+
{draft && n !== undefined ? (
|
|
77
|
+
sure === n ? (
|
|
78
|
+
<div className="flex gap-1"><Button size="sm" loading={restore.loading} onClick={() => void restore.mutate(n)}>Replace the draft</Button><Button size="sm" variant="ghost" onClick={() => setSure(null)}>Cancel</Button></div>
|
|
79
|
+
) : <Button size="sm" variant="outline" aria-label={`Restore version ${n}`} onClick={() => setSure(n)}>Restore</Button>
|
|
80
|
+
) : null}
|
|
81
|
+
</li>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</ul>
|
|
85
|
+
)}
|
|
86
|
+
{restore.error ? <ApiError error={restore.error} title="Could not restore that version" /> : null}
|
|
87
|
+
</SectionCard>
|
|
88
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
89
|
+
</div>
|
|
90
|
+
<ConfirmSpendDialog {...dialog} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoreRows: the honest end of a list that the API pages. Shows how many rows are on screen and, while the API has
|
|
5
|
+
* another page, a "Load more" button. A list never ends silently: either every row is reachable or this says there are more.
|
|
6
|
+
* (Same contract as the foundation's LoadMore; kept local so the newsletter block does not wait on it.)
|
|
7
|
+
*
|
|
8
|
+
* SDK operations: none.
|
|
9
|
+
*/
|
|
10
|
+
import { Button } from "@graph8/sdk/ui";
|
|
11
|
+
|
|
12
|
+
export interface MoreRowsProps {
|
|
13
|
+
/** Rows on screen. */
|
|
14
|
+
shown: number;
|
|
15
|
+
/** What the rows are, plural: "people". */
|
|
16
|
+
noun: string;
|
|
17
|
+
hasMore: boolean;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
onMore: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function MoreRows({ shown, noun, hasMore, loading, onMore }: MoreRowsProps) {
|
|
23
|
+
if (!hasMore) return null;
|
|
24
|
+
return (
|
|
25
|
+
<div className="text-muted-foreground flex items-center justify-between gap-3 py-2 text-sm">
|
|
26
|
+
<span role="status">Showing {shown.toLocaleString()} {noun}</span>
|
|
27
|
+
<Button type="button" variant="outline" size="sm" loading={loading} onClick={onMore}>Load more</Button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -81,7 +81,7 @@ export function NewsletterEditor({ initialNewsletterId, onSelectIssue }: Newslet
|
|
|
81
81
|
{!current ? <EmptyState title="Pick a newsletter" description="Choose a newsletter to see its issues, or create one." /> : (
|
|
82
82
|
<>
|
|
83
83
|
<div className="flex items-center justify-between">
|
|
84
|
-
<h2 className="text-lg font-
|
|
84
|
+
<h2 className="text-lg font-medium">{current.name} <span className="text-muted-foreground text-sm font-normal">{current.status}</span></h2>
|
|
85
85
|
{live ? <Button variant="outline" size="sm" loading={pause.loading} onClick={() => void pause.mutate(undefined)}>Pause newsletter</Button> : <Button size="sm" onClick={() => setPending({ kind: "activate" })}>Activate newsletter</Button>}
|
|
86
86
|
</div>
|
|
87
87
|
{pause.error ? <ApiError error={pause.error} title="Could not pause" /> : null}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NewsletterListSyncs: keep subscribers in step with your lists. A synced list adds its contacts as subscribers; run a
|
|
5
|
+
* sync now to pull in the latest, or turn it off.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. listNewsletterListSyncs GET /newsletters/{id}/list-syncs (read)
|
|
9
|
+
* 2. listLists GET /lists?page&limit&search (paged, "Load more") (read; the lists you can turn on)
|
|
10
|
+
* 3. enableNewsletterListSync POST /newsletters/{id}/list-syncs { audience_list_id, attested_text } (write, confirmed: people are subscribed to a newsletter)
|
|
11
|
+
* 4. runNewsletterListSync POST /newsletters/{id}/list-syncs/{list}/run (write, confirmed: adds the list's people now)
|
|
12
|
+
* 5. disableNewsletterListSync DELETE /newsletters/{id}/list-syncs/{list} (destructive, confirmed: stops syncing; subscribers stay)
|
|
13
|
+
*
|
|
14
|
+
* Turning a sync on subscribes real people, so the person confirms the consent statement first: the exact text is shown
|
|
15
|
+
* and sent as `attested_text`. The consent claim is theirs; the dialog does not pretend otherwise.
|
|
16
|
+
*/
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
24
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
27
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import { asRec, num, pageOf, rowsOf, str } from "../lib/newsletter-extra";
|
|
29
|
+
import type { Rec } from "../lib/newsletter-extra";
|
|
30
|
+
import { MoreRows } from "./more-rows";
|
|
31
|
+
|
|
32
|
+
export interface NewsletterListSyncsProps {
|
|
33
|
+
newsletterId: string;
|
|
34
|
+
/** Called after a sync changed who is subscribed. */
|
|
35
|
+
onChanged?: () => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CONSENT_TEXT = "I confirm the people in this list agreed to receive this newsletter.";
|
|
40
|
+
const PAGE = 50;
|
|
41
|
+
|
|
42
|
+
export function NewsletterListSyncs({ newsletterId, onChanged, className }: NewsletterListSyncsProps) {
|
|
43
|
+
const base = `/newsletters/${encodeURIComponent(newsletterId)}/list-syncs`;
|
|
44
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
45
|
+
const [search, setSearch] = useState("");
|
|
46
|
+
const [note, setNote] = useState("");
|
|
47
|
+
const syncs = useG8Query((signal) => g8Get(base, undefined, { signal }), [newsletterId]);
|
|
48
|
+
const rows = rowsOf<Rec>(syncs.data?.data, "list_syncs");
|
|
49
|
+
// Disabled rows stay in the API list (so they can be turned back on): only enabled ones count as synced.
|
|
50
|
+
const synced = new Set(rows.filter((r) => r.enabled !== false).map((r) => String(r.audience_list_id)));
|
|
51
|
+
const lists = usePaginated<Rec>(async (cursor, signal) => {
|
|
52
|
+
const page = cursor ? Number(cursor) : 1;
|
|
53
|
+
const res = await g8Get("/lists", { limit: PAGE, page, search: search.trim() || undefined }, { signal });
|
|
54
|
+
const got = rowsOf<Rec>(res.data, "lists");
|
|
55
|
+
const more = (res.pagination as { has_next?: boolean } | undefined)?.has_next ?? got.length >= PAGE;
|
|
56
|
+
return pageOf(got, more && got.length > 0 ? page + 1 : null);
|
|
57
|
+
}, [search]);
|
|
58
|
+
const changed = (): void => { void syncs.refetch(); onChanged?.(); };
|
|
59
|
+
|
|
60
|
+
const enable = (l: { id?: unknown; title?: unknown; name?: unknown }): void => ask({
|
|
61
|
+
tier: "external", title: `Subscribe "${str(l.title) || str(l.name)}" to this newsletter`, confirmLabel: "Turn on sync",
|
|
62
|
+
description: <><p>People in this list become subscribers and will receive issues. You are confirming:</p><p className="mt-2 font-medium">{CONSENT_TEXT}</p></>,
|
|
63
|
+
run: () => g8Post(base, { audience_list_id: Number(l.id), attested_text: CONSENT_TEXT }), onDone: changed,
|
|
64
|
+
});
|
|
65
|
+
const run = (s: Rec): void => ask({
|
|
66
|
+
tier: "external", title: `Sync "${str(s.name)}" now`, confirmLabel: "Sync now",
|
|
67
|
+
recipients: num(s.contact_count),
|
|
68
|
+
description: "Adds the list's people who are not subscribed yet. People who unsubscribed stay unsubscribed.",
|
|
69
|
+
run: async () => { await g8Post(`${base}/${encodeURIComponent(String(s.audience_list_id))}/run`); setNote(`Synced "${str(s.name)}".`); }, onDone: changed,
|
|
70
|
+
});
|
|
71
|
+
const disable = (s: Rec): void => ask({
|
|
72
|
+
tier: "destructive", title: `Stop syncing "${str(s.name)}"`, confirmLabel: "Stop syncing",
|
|
73
|
+
description: "New contacts added to this list are no longer subscribed. People already subscribed stay subscribed.",
|
|
74
|
+
tolerateStatus: [404], run: () => g8Delete(`${base}/${encodeURIComponent(String(s.audience_list_id))}`), onDone: changed,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={className}>
|
|
79
|
+
<div className="grid gap-4">
|
|
80
|
+
<SectionCard title="Synced lists" description="Subscribers are kept in step with these lists.">
|
|
81
|
+
{syncs.error ? <ApiError error={syncs.error} title="Could not load the synced lists" onRetry={() => void syncs.refetch()} /> : rows.length === 0 ? (
|
|
82
|
+
<EmptyState compact title="No lists are synced" description="Choose a list below to subscribe its people." />
|
|
83
|
+
) : (
|
|
84
|
+
<ul className="grid gap-2">
|
|
85
|
+
{rows.map((s) => (
|
|
86
|
+
<li key={String(s.audience_list_id)} className="flex items-center justify-between gap-3 rounded-md border p-3">
|
|
87
|
+
<div className="min-w-0"><span className="block truncate text-sm font-medium">{str(s.name) || `List ${String(s.audience_list_id)}`}</span><span className="text-muted-foreground text-xs">{num(s.contact_count) == null ? "" : `${(s.contact_count as number).toLocaleString()} contacts`}</span></div>
|
|
88
|
+
{s.enabled === false ? <StatusPill tone="neutral">Off</StatusPill> : <StatusPill tone="positive">On</StatusPill>}
|
|
89
|
+
{s.enabled === false ? (
|
|
90
|
+
<Button size="sm" variant="outline" aria-label={`Turn ${str(s.name)} back on`} onClick={() => enable({ id: s.audience_list_id, name: s.name })}>Turn on sync</Button>
|
|
91
|
+
) : <div className="flex gap-1"><Button size="sm" variant="outline" aria-label={`Sync ${str(s.name)} now`} onClick={() => run(s)}>Sync now</Button><Button size="sm" variant="ghost" aria-label={`Stop syncing ${str(s.name)}`} onClick={() => disable(s)}>Stop</Button></div>}
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
)}
|
|
96
|
+
{note ? <p role="status" className="mt-2 text-sm">{note}</p> : null}
|
|
97
|
+
</SectionCard>
|
|
98
|
+
<SectionCard title="Add a list" description="Find the list to subscribe.">
|
|
99
|
+
<Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} className="mb-3 max-w-sm" />
|
|
100
|
+
{lists.error ? <ApiError error={lists.error} title="Could not load your lists" onRetry={() => void lists.reload()} /> : lists.loading ? <p className="text-muted-foreground text-sm" aria-busy="true">Loading lists...</p> : lists.items.length === 0 ? (
|
|
101
|
+
<EmptyState compact title={search ? "No list matches" : "You have no lists"} description={search ? "Try another name." : "Create a list in Lists first."} />
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
<ul className="grid gap-2">
|
|
105
|
+
{lists.items.map((l) => (
|
|
106
|
+
<li key={String(l.id)} className="flex items-center justify-between gap-3 rounded-md border p-3">
|
|
107
|
+
<span className="truncate text-sm">{str(l.title)}</span>
|
|
108
|
+
{synced.has(String(l.id)) ? <StatusPill tone="positive">Synced</StatusPill> : <Button size="sm" variant="outline" aria-label={`Sync ${str(l.title)}`} onClick={() => enable(l)}>Turn on sync</Button>}
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
<MoreRows shown={lists.items.length} noun="lists" hasMore={lists.hasMore} loading={lists.loadingMore} onMore={() => void lists.loadMore()} />
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</SectionCard>
|
|
116
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
117
|
+
</div>
|
|
118
|
+
<ConfirmSpendDialog {...dialog} />
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Composes: ScreenLayout (title, tabs, stats), Toolbar, SplitView + DetailPanel (selected issue, deep link
|
|
7
7
|
* `?id=`), FormSheet (new newsletter), IssueForm (write, AI, test, delete), IssueScheduler (send, schedule,
|
|
8
|
-
* pause, resume, cancel), SubscriberTable (add, import a list, verify, unsubscribe, erase), NewsletterStats
|
|
8
|
+
* pause, resume, cancel), SubscriberTable (add, import a list, verify, unsubscribe, erase), NewsletterStats,
|
|
9
|
+
* NewsletterSettings (name, sender, opt-in, sending domain, running state), NewsletterListSyncs, NewsletterTemplates,
|
|
10
|
+
* IssueVersions (history, restore, duplicate) and SubscriberTimeline.
|
|
9
11
|
*
|
|
10
12
|
* Every record has a full detail view: `?tab=` picks Issues, Subscribers, Performance or Details (the newsletter itself);
|
|
11
|
-
* `?id=` opens an issue (Write, Send, Results, Details) or a subscriber (all fields, unsubscribe, erase);
|
|
13
|
+
* `?id=` opens an issue (Write, Send, Results, History, Details) or a subscriber (all fields, activity, unsubscribe, erase);
|
|
14
|
+
* the Settings, Lists and Templates tabs manage the newsletter itself, its synced lists and its saved templates;
|
|
12
15
|
* `&view=full` shows the open record as a full page.
|
|
13
16
|
*
|
|
14
17
|
* User flows: pick or create a newsletter (`?newsletter=`), activate or pause it, read the headline numbers,
|
|
@@ -39,12 +42,18 @@ import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
|
39
42
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
40
43
|
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
41
44
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
45
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
42
46
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
43
47
|
import { isDraft, issueTitle, percent } from "../lib/newsletter-types";
|
|
44
48
|
import type { Issue, IssueKpis, Newsletter, Subscriber } from "../lib/newsletter-types";
|
|
45
49
|
import { IssueForm } from "./issue-form";
|
|
46
50
|
import { IssueScheduler } from "./issue-scheduler";
|
|
51
|
+
import { IssueVersions } from "./issue-versions";
|
|
52
|
+
import { NewsletterListSyncs } from "./newsletter-list-syncs";
|
|
53
|
+
import { NewsletterSettings } from "./newsletter-settings";
|
|
47
54
|
import { NewsletterStats } from "./newsletter-stats";
|
|
55
|
+
import { NewsletterTemplates } from "./newsletter-templates";
|
|
56
|
+
import { SubscriberTimeline } from "./subscriber-timeline";
|
|
48
57
|
import { SubscriberTable } from "./subscriber-table";
|
|
49
58
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
50
59
|
|
|
@@ -58,7 +67,9 @@ export interface NewsletterScreenProps {
|
|
|
58
67
|
className?: string;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
type Tab = "issues" | "subscribers" | "performance" | "details";
|
|
70
|
+
type Tab = "issues" | "subscribers" | "performance" | "details" | "settings" | "lists" | "templates";
|
|
71
|
+
/** Subscribers per read when looking one up for a deep link; every page is read. */
|
|
72
|
+
const SUBSCRIBER_PAGE = 100; // the API caps page_size at 100 (a larger value is a 422)
|
|
62
73
|
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
63
74
|
|
|
64
75
|
function tone(status: string | undefined): Tone {
|
|
@@ -119,7 +130,7 @@ function IssueDetails({ issue }: { issue: Issue }) {
|
|
|
119
130
|
);
|
|
120
131
|
}
|
|
121
132
|
|
|
122
|
-
function SubscriberDetail({ newsletterId, subscriber, onChanged, onClose, expandProps }: { newsletterId: string; subscriber: Subscriber; onChanged: () => void; onClose: () => void; expandProps: { onExpand: () => void; onCollapse?: () => void } }) {
|
|
133
|
+
function SubscriberDetail({ newsletterId, subscriber, issues, onChanged, onClose, expandProps }: { newsletterId: string; subscriber: Subscriber; issues?: Issue[]; onChanged: () => void; onClose: () => void; expandProps: { onExpand: () => void; onCollapse?: () => void } }) {
|
|
123
134
|
const [confirm, setConfirm] = useState<"unsubscribe" | "erase" | null>(null);
|
|
124
135
|
const base = `/newsletters/${encodeURIComponent(newsletterId)}/subscribers/${subscriber.id}`;
|
|
125
136
|
const run = useG8Mutation((kind: "unsubscribe" | "erase") => (kind === "unsubscribe" ? g8Patch(`${base}/unsubscribe`, {}) : g8Delete(base)), {
|
|
@@ -155,6 +166,7 @@ function SubscriberDetail({ newsletterId, subscriber, onChanged, onClose, expand
|
|
|
155
166
|
...extra.map(([key, v]) => ({ label: key.replace(/_/g, " "), value: String(v) })),
|
|
156
167
|
]}
|
|
157
168
|
/>
|
|
169
|
+
{issues ? <SubscriberTimeline className="mt-4" newsletterId={newsletterId} subscriberId={subscriber.id} issues={issues} /> : null}
|
|
158
170
|
<ConfirmSpendDialog
|
|
159
171
|
open={confirm !== null}
|
|
160
172
|
tier={confirm === "erase" ? "destructive" : "external"}
|
|
@@ -177,7 +189,7 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
177
189
|
const [view, setView] = useUrlState("view", "");
|
|
178
190
|
const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
|
|
179
191
|
const [tabParam, setTabParam] = useUrlState("tab", "issues");
|
|
180
|
-
const tab = (["issues", "subscribers", "performance", "details"].includes(tabParam) ? tabParam : "issues") as Tab;
|
|
192
|
+
const tab = (["issues", "subscribers", "performance", "details", "settings", "lists", "templates"].includes(tabParam) ? tabParam : "issues") as Tab;
|
|
181
193
|
const closeDetail = (): void => { setIssueParam(null); setView(null); };
|
|
182
194
|
const [subRefresh, setSubRefresh] = useState(0);
|
|
183
195
|
const [openedSubscriber, setOpenedSubscriber] = useState<Subscriber | null>(null);
|
|
@@ -187,6 +199,8 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
187
199
|
const [name, setName] = useState("");
|
|
188
200
|
const [goal, setGoal] = useState("");
|
|
189
201
|
const [pending, setPending] = useState<"create" | "activate" | null>(null);
|
|
202
|
+
// Bumped after a version restore so the editor reloads the draft instead of showing the old text.
|
|
203
|
+
const [formRev, setFormRev] = useState(0);
|
|
190
204
|
|
|
191
205
|
const newsletters = useG8Query((signal) => g8Get<Newsletter[]>("/newsletters", {}, { signal }).then((r) => r.data ?? []), []);
|
|
192
206
|
const list = newsletters.data ?? [];
|
|
@@ -209,12 +223,17 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
209
223
|
const pause = useG8Mutation(() => g8Post(`${base}/pause`), { onSuccess: () => void newsletters.refetch() });
|
|
210
224
|
|
|
211
225
|
const subscriberId = tab === "subscribers" && issueParam ? Number(issueParam) : undefined;
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
// A deep link to a subscriber reads every page (100 per read), so no subscriber is out of reach.
|
|
227
|
+
const lookup = useAllPages<Subscriber>(
|
|
228
|
+
async (page, signal) => {
|
|
229
|
+
const r = await g8Get<{ items?: Subscriber[]; total?: number }>(`${base}/subscribers`, { page, page_size: SUBSCRIBER_PAGE }, { signal });
|
|
230
|
+
const rows = r.data?.items ?? [];
|
|
231
|
+
return { rows, hasNext: r.data?.total != null ? page * SUBSCRIBER_PAGE < r.data.total : rows.length >= SUBSCRIBER_PAGE };
|
|
232
|
+
},
|
|
214
233
|
[base, subRefresh],
|
|
215
|
-
{ enabled: subscriberId !== undefined && openedSubscriber?.id !== subscriberId },
|
|
234
|
+
{ enabled: Boolean(newsletterId) && subscriberId !== undefined && openedSubscriber?.id !== subscriberId },
|
|
216
235
|
);
|
|
217
|
-
const openSubscriber = subscriberId === undefined ? null : openedSubscriber?.id === subscriberId ? openedSubscriber : (
|
|
236
|
+
const openSubscriber = subscriberId === undefined ? null : openedSubscriber?.id === subscriberId ? openedSubscriber : (lookup.items.find((x) => x.id === subscriberId) ?? null);
|
|
218
237
|
const all = issues.data ?? [];
|
|
219
238
|
const sent = all.filter((i) => i.status === "sent" || i.sent_at);
|
|
220
239
|
const totals = sent.reduce((a, i) => ({ delivered: a.delivered + (i.kpis?.delivered ?? 0), opened: a.opened + (i.kpis?.opened ?? 0), clicked: a.clicked + (i.kpis?.clicked ?? 0) }), { delivered: 0, opened: 0, clicked: 0 });
|
|
@@ -227,9 +246,10 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
227
246
|
const pickNewsletter = (id: string): void => { setIssueParam(null); setUrlNewsletter(id); };
|
|
228
247
|
const detailTabs = selected
|
|
229
248
|
? [
|
|
230
|
-
{ id: "write", label: isDraft(selected) ? "Write" : "Content", content: <IssueForm key={`f${selected.id}`} newsletterId={newsletterId} issueId={selected.id} onSaved={() => void issues.refetch()} onDeleted={() => { setIssueParam(null); void issues.refetch(); }} /> },
|
|
249
|
+
{ id: "write", label: isDraft(selected) ? "Write" : "Content", content: <IssueForm key={`f${selected.id}:${formRev}`} newsletterId={newsletterId} issueId={selected.id} onSaved={() => void issues.refetch()} onDeleted={() => { setIssueParam(null); void issues.refetch(); }} /> },
|
|
231
250
|
{ id: "send", label: "Send", content: <IssueScheduler key={`s${selected.id}`} newsletterId={newsletterId} issueId={selected.id} onSent={() => void issues.refetch()} /> },
|
|
232
251
|
{ id: "results", label: "Results", content: <IssueResults newsletterId={newsletterId} issue={selected} /> },
|
|
252
|
+
{ id: "history", label: "History", content: <IssueVersions key={`v${selected.id}:${formRev}`} newsletterId={newsletterId} issue={selected} onRestored={() => { setFormRev((n) => n + 1); void issues.refetch(); }} onDuplicated={(id) => { void issues.refetch(); setIssueParam(String(id)); }} /> },
|
|
233
253
|
{ id: "details", label: "Issue details", content: <IssueDetails issue={selected} /> },
|
|
234
254
|
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
|
|
235
255
|
]
|
|
@@ -287,9 +307,9 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
287
307
|
detailWidth="lg"
|
|
288
308
|
list={<SubscriberTable key={newsletterId} newsletterId={newsletterId} refreshKey={subRefresh} selectedId={subscriberId} onOpen={(sub) => { setOpenedSubscriber(sub); setIssueParam(String(sub.id)); }} />}
|
|
289
309
|
detail={openSubscriber ? (
|
|
290
|
-
<SubscriberDetail key={openSubscriber.id} newsletterId={newsletterId} subscriber={openSubscriber} onChanged={() => { setSubRefresh((n) => n + 1); setOpenedSubscriber(null); }} onClose={() => { closeDetail(); setOpenedSubscriber(null); }} expandProps={expandProps} />
|
|
310
|
+
<SubscriberDetail key={openSubscriber.id} newsletterId={newsletterId} subscriber={openSubscriber} issues={all} onChanged={() => { setSubRefresh((n) => n + 1); setOpenedSubscriber(null); }} onClose={() => { closeDetail(); setOpenedSubscriber(null); }} expandProps={expandProps} />
|
|
291
311
|
) : lookup.loading ? <Skeleton className="h-40 w-full" aria-busy="true" /> : (
|
|
292
|
-
<EmptyState title="Subscriber not found" description="Open a subscriber from the list.
|
|
312
|
+
<EmptyState title="Subscriber not found" description="Open a subscriber from the list." />
|
|
293
313
|
)}
|
|
294
314
|
/>
|
|
295
315
|
)
|
|
@@ -320,6 +340,9 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
320
340
|
</DetailPanel>
|
|
321
341
|
)
|
|
322
342
|
: tab === "performance" ? <NewsletterStats key={newsletterId} newsletterId={newsletterId} />
|
|
343
|
+
: tab === "settings" ? <NewsletterSettings key={newsletterId} newsletterId={newsletterId} onChanged={() => void newsletters.refetch()} />
|
|
344
|
+
: tab === "lists" ? <NewsletterListSyncs key={newsletterId} newsletterId={newsletterId} onChanged={() => setSubRefresh((n) => n + 1)} />
|
|
345
|
+
: tab === "templates" ? <NewsletterTemplates key={newsletterId} newsletterId={newsletterId} onUsed={(id) => { void issues.refetch(); setTabParam(null); setIssueParam(String(id)); }} />
|
|
323
346
|
: (
|
|
324
347
|
<SplitView
|
|
325
348
|
detailOpen={detailOpen}
|
|
@@ -364,7 +387,7 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
364
387
|
<Button size="sm" onClick={() => setSheet(true)}><Plus className="size-4" />New newsletter</Button>
|
|
365
388
|
</>
|
|
366
389
|
)}
|
|
367
|
-
tabs={current ? [{ id: "issues", label: "Issues", count: all.length }, { id: "subscribers", label: "Subscribers", count: audience.data }, { id: "performance", label: "Performance" }, { id: "details", label: "Details" }] : undefined}
|
|
390
|
+
tabs={current ? [{ id: "issues", label: "Issues", count: all.length }, { id: "subscribers", label: "Subscribers", count: audience.data }, { id: "performance", label: "Performance" }, { id: "templates", label: "Templates" }, { id: "lists", label: "Lists" }, { id: "settings", label: "Settings" }, { id: "details", label: "Details" }] : undefined}
|
|
368
391
|
activeTab={tab}
|
|
369
392
|
onTabChange={(id) => { setTabParam(id === "issues" ? null : id); closeDetail(); setOpenedSubscriber(null); }}
|
|
370
393
|
stats={current ? [
|