@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
package/blocks/lists/block.json
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"finished ListsScreen and SuppressionsScreen pages"
|
|
13
13
|
],
|
|
14
14
|
"dependsOn": [
|
|
15
|
-
"crm"
|
|
15
|
+
"crm",
|
|
16
|
+
"pickers"
|
|
16
17
|
],
|
|
17
18
|
"operations": [
|
|
18
19
|
"add_contacts_to_list_lists__list_id__contacts_post",
|
|
@@ -22,6 +23,7 @@
|
|
|
22
23
|
"create_list_lists_post",
|
|
23
24
|
"delete_list_lists__list_id__delete",
|
|
24
25
|
"download_list_lists__audience_id__download_get",
|
|
26
|
+
"get_contact_lists_contacts__contact_id__lists_get",
|
|
25
27
|
"list_contact_fields_fields_get",
|
|
26
28
|
"list_contacts_contacts_get",
|
|
27
29
|
"list_lists_lists_get",
|
|
@@ -86,7 +88,8 @@
|
|
|
86
88
|
"list_suppressions_contacts_suppressions_get",
|
|
87
89
|
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
88
90
|
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
89
|
-
"reinstate_suppressions_contacts_suppressions_reinstate_post"
|
|
91
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post",
|
|
92
|
+
"list_contacts_contacts_get"
|
|
90
93
|
],
|
|
91
94
|
"tiers": [
|
|
92
95
|
"read",
|
|
@@ -121,12 +124,37 @@
|
|
|
121
124
|
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
122
125
|
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
123
126
|
"list_suppressions_contacts_suppressions_get",
|
|
124
|
-
"reinstate_suppressions_contacts_suppressions_reinstate_post"
|
|
127
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post",
|
|
128
|
+
"list_contacts_contacts_get"
|
|
125
129
|
],
|
|
126
130
|
"tiers": [
|
|
127
131
|
"read",
|
|
128
132
|
"write"
|
|
129
133
|
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"name": "AddCompanyPeopleToListButton",
|
|
137
|
+
"file": "components/add-company-people-to-list-button.tsx",
|
|
138
|
+
"purpose": "\"Add people to list\" for one or more companies: reads the people at them, then AddToListButton picks the list and adds them.",
|
|
139
|
+
"operations": [
|
|
140
|
+
"list_contacts_contacts_get"
|
|
141
|
+
],
|
|
142
|
+
"tiers": [
|
|
143
|
+
"read"
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "RecordListsTab",
|
|
148
|
+
"file": "components/record-lists-tab.tsx",
|
|
149
|
+
"purpose": "The lists one contact belongs to, with Add to list and a confirmed Remove per list; an extraDetailTabs entry for ContactsScreen.",
|
|
150
|
+
"operations": [
|
|
151
|
+
"get_contact_lists_contacts__contact_id__lists_get",
|
|
152
|
+
"remove_contacts_from_list_lists__list_id__contacts_delete"
|
|
153
|
+
],
|
|
154
|
+
"tiers": [
|
|
155
|
+
"read",
|
|
156
|
+
"destructive"
|
|
157
|
+
]
|
|
130
158
|
}
|
|
131
159
|
],
|
|
132
160
|
"hooks": [],
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AddCompanyPeopleToListButton: "Add people to list" for one or more companies. Lists hold contacts, so this
|
|
5
|
+
* reads the people at the companies and hands them to AddToListButton's dialog (list picker, conflict review).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listContacts GET /contacts?company_id=&page=&limit=100 (read; every page, capped at 500 people)
|
|
9
|
+
* 2. then AddToListButton: listLists / createList, addContactsToList (write, chosen in the dialog)
|
|
10
|
+
*
|
|
11
|
+
* Reading people is free and adds nothing until the person picks a list and confirms in the dialog.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
18
|
+
import { AddToListButton } from "./add-to-list-button";
|
|
19
|
+
import type { AddToListResult } from "./add-to-list-button";
|
|
20
|
+
|
|
21
|
+
export interface AddCompanyPeopleToListButtonProps {
|
|
22
|
+
companyIds: number[];
|
|
23
|
+
label?: string;
|
|
24
|
+
variant?: "default" | "outline" | "secondary" | "ghost";
|
|
25
|
+
size?: "default" | "sm";
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
onAdded?: (result: AddToListResult) => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PEOPLE_CAP = 500;
|
|
32
|
+
|
|
33
|
+
interface PeopleEnvelope { data?: Array<{ id: number }>; pagination?: { has_next?: boolean } }
|
|
34
|
+
|
|
35
|
+
/** Ids of the people at these companies, every page, capped. */
|
|
36
|
+
async function peopleAt(companyIds: number[]): Promise<number[]> {
|
|
37
|
+
const ids = new Set<number>();
|
|
38
|
+
for (const companyId of companyIds) {
|
|
39
|
+
for (let page = 1; page <= 10 && ids.size < PEOPLE_CAP; page++) {
|
|
40
|
+
const env = (await g8Get<Array<{ id: number }>>("/contacts", { company_id: companyId, page, limit: 100 })) as PeopleEnvelope;
|
|
41
|
+
for (const c of env.data ?? []) ids.add(c.id);
|
|
42
|
+
if (!env.pagination?.has_next || (env.data ?? []).length === 0) break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...ids].slice(0, PEOPLE_CAP);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function AddCompanyPeopleToListButton({ companyIds, label = "Add people to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddCompanyPeopleToListButtonProps) {
|
|
49
|
+
const [loading, setLoading] = useState(false);
|
|
50
|
+
const [error, setError] = useState<unknown>(null);
|
|
51
|
+
const [people, setPeople] = useState<number[] | null>(null);
|
|
52
|
+
|
|
53
|
+
const start = async (): Promise<void> => {
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
try {
|
|
57
|
+
setPeople(await peopleAt(companyIds));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
setError(e);
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<Button className={className} variant={variant} size={size} loading={loading} disabled={disabled || companyIds.length === 0} onClick={() => void start()}>{label}</Button>
|
|
68
|
+
{error ? <ApiError error={error} title="Could not read the people" onRetry={() => void start()} /> : null}
|
|
69
|
+
{people && people.length === 0 ? <p role="status" className="text-xs text-muted-foreground">No people found at {companyIds.length === 1 ? "this company" : "these companies"}.</p> : null}
|
|
70
|
+
{people && people.length > 0 ? <AddToListButton key={people.join(",")} contactIds={people} defaultOpen hideTrigger onAdded={onAdded} onClosed={() => setPeople(null)} /> : null}
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -42,6 +42,12 @@ export interface AddToListButtonProps {
|
|
|
42
42
|
disabled?: boolean;
|
|
43
43
|
onAdded?: (result: AddToListResult) => void;
|
|
44
44
|
className?: string;
|
|
45
|
+
/** Start with the dialog open (used when another control resolved the contact ids first). */
|
|
46
|
+
defaultOpen?: boolean;
|
|
47
|
+
/** Render the dialog without its trigger button; pair with `defaultOpen` and `onClosed`. */
|
|
48
|
+
hideTrigger?: boolean;
|
|
49
|
+
/** Runs after the dialog closed (cancel, done or Escape). */
|
|
50
|
+
onClosed?: () => void;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
interface ConflictDetail { code?: string; ready_count?: number; warned?: unknown[]; blocked?: unknown[]; message?: string }
|
|
@@ -52,26 +58,30 @@ function conflictOf(error: unknown): ConflictDetail | null {
|
|
|
52
58
|
return e?.status === 409 && e.detail && typeof e.detail === "object" && e.detail.code === "CONFLICT_REVIEW_REQUIRED" ? e.detail : null;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddToListButtonProps) {
|
|
56
|
-
const [open, setOpen] = useState(
|
|
61
|
+
export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className, defaultOpen = false, hideTrigger = false, onClosed }: AddToListButtonProps) {
|
|
62
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
57
63
|
const [target, setTarget] = useState<ListSummary | null>(null);
|
|
58
64
|
const [result, setResult] = useState<AddToListResult | null>(null);
|
|
59
65
|
const destination = listId ?? target?.id;
|
|
60
66
|
|
|
61
67
|
const add = useG8Mutation(async (resolution?: Resolution) => {
|
|
62
|
-
const res = await g8Post<{ added
|
|
68
|
+
const res = await g8Post<{ added?: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
|
|
63
69
|
contact_ids: contactIds,
|
|
64
70
|
...(resolution ? { conflict_resolution: resolution } : {}),
|
|
65
71
|
});
|
|
66
|
-
|
|
72
|
+
const already = res.data.already_member ?? [];
|
|
73
|
+
const missing = res.data.not_found ?? [];
|
|
74
|
+
// The response is an open dict: when it carries no count, derive it instead of rendering "undefined".
|
|
75
|
+
const added = typeof res.data.added === "number" ? res.data.added : Math.max(contactIds.length - already.length - missing.length, 0);
|
|
76
|
+
return { listId: destination as number, added, already_member: already, not_found: missing };
|
|
67
77
|
}, { invalidates: [["lists"], ["contacts", "list"]], onSuccess: (r) => { setResult(r); onAdded?.(r); } });
|
|
68
78
|
|
|
69
79
|
const conflict = conflictOf(add.error);
|
|
70
|
-
const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); };
|
|
80
|
+
const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); onClosed?.(); };
|
|
71
81
|
|
|
72
82
|
return (
|
|
73
83
|
<>
|
|
74
|
-
<Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>
|
|
84
|
+
{hideTrigger ? null : <Button className={className} variant={variant} size={size} disabled={disabled || contactIds.length === 0} onClick={() => setOpen(true)}>{label}</Button>}
|
|
75
85
|
<Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
|
76
86
|
<DialogContent>
|
|
77
87
|
<DialogHeader>
|
|
@@ -89,7 +99,7 @@ export function AddToListButton({ contactIds, listId, label = "Add to list", var
|
|
|
89
99
|
<>
|
|
90
100
|
{listId == null ? <ListPicker value={target?.id} onChange={(l) => { setTarget(l); add.reset(); }} /> : null}
|
|
91
101
|
{conflict ? (
|
|
92
|
-
<div role="alert" className="rounded-md border border-
|
|
102
|
+
<div role="alert" className="rounded-md border border-warning-foreground/30 bg-warning p-3 text-sm">
|
|
93
103
|
<p>{conflict.message ?? "Some contacts belong to other reps."}</p>
|
|
94
104
|
<p className="mt-1">{conflict.ready_count ?? 0} ready, {conflict.warned?.length ?? 0} flagged, {conflict.blocked?.length ?? 0} blocked (never added).</p>
|
|
95
105
|
<div className="mt-2 flex gap-2">
|
|
@@ -16,7 +16,9 @@ import { useEffect, useState } from "react";
|
|
|
16
16
|
import { Button, Input } from "@graph8/sdk/ui";
|
|
17
17
|
import { ApiError } from "@/g8/components/api-error";
|
|
18
18
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
-
import {
|
|
19
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
20
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
21
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
20
22
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
21
23
|
|
|
22
24
|
export interface ListSummary {
|
|
@@ -59,17 +61,24 @@ function useDebounced<T>(value: T, ms = 250): T {
|
|
|
59
61
|
return v;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/** Lists per read; "Load more" appends the next page (the picker used to stop silently at 50). */
|
|
65
|
+
const LIST_PAGE = 50;
|
|
66
|
+
|
|
62
67
|
export function ListPicker({ value, onChange, type = "contacts", allowCreate = true, suggestedName, className }: ListPickerProps) {
|
|
63
68
|
const [search, setSearch] = useState("");
|
|
64
69
|
const [newName, setNewName] = useState(suggestedName ?? "");
|
|
65
70
|
const q = useDebounced(search.trim());
|
|
66
|
-
const lists =
|
|
71
|
+
const lists = useLoadMoreList(byPage<ListSummary>(LIST_PAGE, async (page, signal) => {
|
|
72
|
+
const env = await g8Get<ListSummary[]>("/lists", { search: q || undefined, page, limit: LIST_PAGE }, { signal });
|
|
73
|
+
const rows = (env.data ?? []) as ListSummary[];
|
|
74
|
+
return { rows, hasNext: hasNextOf(env, LIST_PAGE, rows.length) };
|
|
75
|
+
}), [q]);
|
|
67
76
|
const create = useG8Mutation((title: string) => g8Post<ListSummary>("/lists", { title, type }), {
|
|
68
77
|
invalidates: [["lists"]],
|
|
69
78
|
onSuccess: (res) => { setNewName(""); void lists.refetch(); onChange({ type, ...res.data, title: res.data.title ?? newName }); },
|
|
70
79
|
});
|
|
71
80
|
|
|
72
|
-
const rows = (
|
|
81
|
+
const rows = (lists.data ?? []).filter((l) => !l.type || l.type === type);
|
|
73
82
|
return (
|
|
74
83
|
<div className={`flex flex-col gap-2 ${className ?? ""}`}>
|
|
75
84
|
<Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
@@ -86,6 +95,7 @@ export function ListPicker({ value, onChange, type = "contacts", allowCreate = t
|
|
|
86
95
|
</li>
|
|
87
96
|
))}
|
|
88
97
|
</ul>
|
|
98
|
+
<LoadMore shown={rows.length} noun="lists" hasMore={lists.hasMore} loading={lists.loadingMore} onLoadMore={lists.loadMore} />
|
|
89
99
|
{allowCreate ? (
|
|
90
100
|
<form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate(newName.trim()); }}>
|
|
91
101
|
<Input aria-label="New list name" placeholder="Or create a new list" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
|
@@ -46,7 +46,7 @@ function count(l: ListSummary): number {
|
|
|
46
46
|
return l.contact_count ?? l.company_count ?? l.total ?? 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-
|
|
49
|
+
const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-warning-foreground" };
|
|
50
50
|
|
|
51
51
|
export function ListTable({ linkParam, pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
|
|
52
52
|
const [search, setSearch] = useState("");
|
|
@@ -67,6 +67,8 @@ export interface ListsScreenProps {
|
|
|
67
67
|
extraDetailTabs?: { id: string; label: string; render: (list: ListSummary) => ReactNode }[];
|
|
68
68
|
/** Where company-list members are managed. Default: the graph8 app. */
|
|
69
69
|
appUrl?: string;
|
|
70
|
+
/** Open a list member in your contacts screen; without it the member rows are not clickable. */
|
|
71
|
+
onOpenContact?: (contactId: number) => void;
|
|
70
72
|
className?: string;
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -99,7 +101,7 @@ function when(value?: string | null): string {
|
|
|
99
101
|
return value ? new Date(value).toLocaleDateString() : "";
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, appUrl: appUrlProp, className }: ListsScreenProps) {
|
|
104
|
+
export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, renderDetailActions, extraDetailTabs, appUrl: appUrlProp, onOpenContact, className }: ListsScreenProps) {
|
|
103
105
|
const brand = useBrand();
|
|
104
106
|
const appUrl = appUrlProp ?? brand.appUrl;
|
|
105
107
|
const [search, setSearch] = useState("");
|
|
@@ -249,7 +251,7 @@ export function ListsScreen({ pageSize = 25, toolbarActions, renderRowActions, r
|
|
|
249
251
|
: (
|
|
250
252
|
<div className="flex flex-col gap-2">
|
|
251
253
|
{removeMembers.error ? <ApiError error={removeMembers.error} title="Could not remove them" /> : null}
|
|
252
|
-
<ContactTable listId={selected.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey}
|
|
254
|
+
<ContactTable listId={selected.id} selectable allowDelete={false} pageSize={10} refreshKey={membersKey} onOpen={onOpenContact ? (c) => onOpenContact(c.id) : undefined}
|
|
253
255
|
renderBulkActions={({ ids }) => <Button size="sm" variant="outline" onClick={() => setRemoving(ids)}>Remove from list</Button>} />
|
|
254
256
|
</div>
|
|
255
257
|
),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordListsTab: the lists one contact belongs to, with "Add to list" and a confirmed "Remove" per list. Use it as an
|
|
5
|
+
* `extraDetailTabs` entry of ContactsScreen:
|
|
6
|
+
* { id: "lists", label: "Lists", render: (c) => <RecordListsTab contactId={c.id} onOpenList={(id) => router.push(`/lists?id=${id}`)} /> }
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getContactLists GET /contacts/{id}/lists (read; the API answers every list the key can see, unpaged)
|
|
10
|
+
* 2. AddToListButton: listLists / createList, addContactsToList (write, chosen in its dialog)
|
|
11
|
+
* 3. removeContactsFromList DELETE /lists/{list_id}/contacts (destructive: confirmed with ConfirmSpendDialog)
|
|
12
|
+
* Removing takes the contact out of that list only; the contact record and its other lists are untouched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { ListChecks } from "lucide-react";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
24
|
+
import { AddToListButton } from "./add-to-list-button";
|
|
25
|
+
|
|
26
|
+
/** One row of GET /contacts/{id}/lists. */
|
|
27
|
+
interface MemberList { audience_id?: number | null; audience_title?: string | null }
|
|
28
|
+
|
|
29
|
+
/** The rows of an API answer, whether it came as `{ items }` or a bare array. */
|
|
30
|
+
function itemsOf<T>(env: unknown): T[] {
|
|
31
|
+
const data = (env as { data?: unknown } | null)?.data;
|
|
32
|
+
if (Array.isArray(data)) return data as T[];
|
|
33
|
+
const items = (data as { items?: unknown } | null)?.items;
|
|
34
|
+
return Array.isArray(items) ? (items as T[]) : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RecordListsTabProps {
|
|
38
|
+
contactId: number;
|
|
39
|
+
/** Route to the list in your lists screen. Without it the rows link to `${listsPath}?id=<list id>`. */
|
|
40
|
+
onOpenList?: (listId: number) => void;
|
|
41
|
+
/** Path of your lists screen for the plain links. Default "/lists". */
|
|
42
|
+
listsPath?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function RecordListsTab({ contactId, onOpenList, listsPath = "/lists" }: RecordListsTabProps) {
|
|
46
|
+
const [removing, setRemoving] = useState<MemberList | null>(null);
|
|
47
|
+
const q = useG8Query(async (signal) => itemsOf<MemberList>(await g8Get(`/contacts/${contactId}/lists`, undefined, { signal })).filter((l) => l.audience_id != null), [contactId]);
|
|
48
|
+
const remove = useG8Mutation(async (list: MemberList) => {
|
|
49
|
+
await g8Delete(`/lists/${list.audience_id}/contacts`, { body: { contact_ids: [contactId] } });
|
|
50
|
+
setRemoving(null);
|
|
51
|
+
await q.refetch();
|
|
52
|
+
}, { invalidates: [["lists"], ["contacts", "list"]], onError: () => setRemoving(null) });
|
|
53
|
+
|
|
54
|
+
const add = <AddToListButton contactIds={[contactId]} onAdded={() => void q.refetch()} />;
|
|
55
|
+
if (q.error) return <ApiError error={q.error} title="Could not load lists" onRetry={() => void q.refetch()} />;
|
|
56
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading lists" />;
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-3">
|
|
59
|
+
<div className="flex items-center justify-between gap-2">
|
|
60
|
+
<p className="text-sm text-muted-foreground">{q.data.length === 1 ? "In 1 list" : `In ${q.data.length.toLocaleString()} lists`}</p>
|
|
61
|
+
{add}
|
|
62
|
+
</div>
|
|
63
|
+
{remove.error ? <ApiError error={remove.error} title="Could not remove from the list" /> : null}
|
|
64
|
+
{q.data.length === 0 ? (
|
|
65
|
+
<EmptyState compact icon={<ListChecks className="size-8" />} title="Not in any list" description="Add this contact to a list to sequence or sync it with others." />
|
|
66
|
+
) : (
|
|
67
|
+
<ul className="divide-y rounded-md border" aria-label="Lists">
|
|
68
|
+
{q.data.map((l) => (
|
|
69
|
+
<li key={l.audience_id} className="flex items-center justify-between gap-3 p-3 text-sm">
|
|
70
|
+
<a
|
|
71
|
+
href={`${listsPath}?id=${encodeURIComponent(String(l.audience_id))}`}
|
|
72
|
+
className="min-w-0 truncate font-medium hover:underline"
|
|
73
|
+
onClick={(e) => {
|
|
74
|
+
if (!onOpenList || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
onOpenList(l.audience_id as number);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{l.audience_title || `List #${l.audience_id}`}
|
|
80
|
+
</a>
|
|
81
|
+
<Button size="sm" variant="ghost" loading={remove.loading && removing?.audience_id === l.audience_id} onClick={() => setRemoving(l)}>Remove</Button>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
)}
|
|
86
|
+
<ConfirmSpendDialog
|
|
87
|
+
open={removing !== null}
|
|
88
|
+
tier="destructive"
|
|
89
|
+
title={`Remove from ${removing?.audience_title || "this list"}`}
|
|
90
|
+
description="The contact leaves this list only. The contact and its other lists are not changed."
|
|
91
|
+
affected={1}
|
|
92
|
+
confirmLabel="Remove"
|
|
93
|
+
confirming={remove.loading}
|
|
94
|
+
onConfirm={() => removing && void remove.mutate(removing)}
|
|
95
|
+
onCancel={() => setRemoving(null)}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* 4. reinstateSuppressions POST /contacts/suppressions/reinstate { contact_ids, reason } (write, but it lets
|
|
12
12
|
* sequences and campaigns contact people again, so it goes through ConfirmSpendDialog as "external")
|
|
13
13
|
*
|
|
14
|
+
* No ids to type: contacts to suppress and the contact to check are chosen with a ContactPicker (search by name or work
|
|
15
|
+
* email; GET /contacts, read). Pasting numeric ids still works under "Paste ids instead" / "Check by id instead".
|
|
14
16
|
* The ledger holds ids, not names or emails (no personal data): resolve a name with ContactDetail.
|
|
15
17
|
* Suppressing only ever REDUCES outreach, so it runs without a confirmation.
|
|
16
18
|
*/
|
|
@@ -20,6 +22,8 @@ import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead,
|
|
|
20
22
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
26
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
23
27
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
28
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
29
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
@@ -59,6 +63,8 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
59
63
|
const [category, setCategory] = useState("");
|
|
60
64
|
const [offset, setOffset] = useState(0);
|
|
61
65
|
const [idsText, setIdsText] = useState("");
|
|
66
|
+
const [chosen, setChosen] = useState<ContactOption[]>([]);
|
|
67
|
+
const [checkChosen, setCheckChosen] = useState<ContactOption | null>(null);
|
|
62
68
|
const [addChannel, setAddChannel] = useState("all");
|
|
63
69
|
const [reason, setReason] = useState("");
|
|
64
70
|
const [picked, setPicked] = useState<Set<number>>(new Set());
|
|
@@ -68,7 +74,7 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
68
74
|
|
|
69
75
|
const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
|
|
70
76
|
const add = useG8Mutation((ids: number[]) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: ids, channel: addChannel, reason: reason.trim() || undefined }), {
|
|
71
|
-
onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
|
|
77
|
+
onSuccess: () => { setIdsText(""); setChosen([]); setReason(""); void ledger.refetch(); },
|
|
72
78
|
});
|
|
73
79
|
const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
|
|
74
80
|
onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); setPicked(new Set()); void ledger.refetch(); },
|
|
@@ -76,15 +82,23 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
76
82
|
const check = useG8Mutation((id: number) => g8Get<ContactStatus>(`/contacts/${id}/suppression`));
|
|
77
83
|
|
|
78
84
|
const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
|
|
79
|
-
const ids = parseIds(idsText);
|
|
85
|
+
const ids = [...new Set([...chosen.map((c) => c.id), ...parseIds(idsText)])];
|
|
86
|
+
const checkTarget = checkChosen?.id ?? Number(checkId);
|
|
80
87
|
const selectedContacts = [...picked];
|
|
81
88
|
const togglePick = (contactId: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(contactId)) n.add(contactId); return n; });
|
|
82
89
|
|
|
83
90
|
return (
|
|
84
91
|
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
85
92
|
<section className="flex flex-col gap-2" aria-label="Suppress contacts">
|
|
86
|
-
<
|
|
87
|
-
<
|
|
93
|
+
<span className="text-sm font-medium leading-none">Who to suppress</span>
|
|
94
|
+
<ContactPicker multiple label="Contacts to suppress" selected={chosen} onSelectedChange={setChosen} />
|
|
95
|
+
<details className="text-sm">
|
|
96
|
+
<summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
|
|
97
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
98
|
+
<Label htmlFor="suppress-ids">Suppress contacts by id</Label>
|
|
99
|
+
<Textarea id="suppress-ids" rows={2} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
|
|
100
|
+
</div>
|
|
101
|
+
</details>
|
|
88
102
|
<div className="flex flex-wrap items-center gap-2">
|
|
89
103
|
<select aria-label="Channel to suppress" className="h-9 rounded-md border bg-background px-2 text-sm" value={addChannel} onChange={(e) => setAddChannel(e.target.value)}>
|
|
90
104
|
<option value="all">All channels</option><option value="email">Email</option><option value="phone">Phone</option><option value="linkedin">LinkedIn</option>
|
|
@@ -97,8 +111,15 @@ export function SuppressionManager({ pageSize = 50, onOpenContact, className }:
|
|
|
97
111
|
</section>
|
|
98
112
|
|
|
99
113
|
<section className="flex items-end gap-2" aria-label="Check a contact">
|
|
100
|
-
<div className="flex flex-col gap-1"
|
|
101
|
-
|
|
114
|
+
<div className="flex flex-col gap-1">
|
|
115
|
+
<span className="text-sm font-medium leading-none">Check one contact</span>
|
|
116
|
+
<ContactPicker label="Contact to check" value={checkChosen} onChange={(c) => { setCheckChosen(c); setCheckId(""); }} />
|
|
117
|
+
<details className="text-sm">
|
|
118
|
+
<summary className="cursor-pointer text-muted-foreground">Check by id instead</summary>
|
|
119
|
+
<div className="mt-1 flex flex-col gap-1"><Label htmlFor="check-id">Check one contact</Label><Input id="check-id" inputMode="numeric" placeholder="Contact id" className="w-40" value={checkId} onChange={(e) => { setCheckId(e.target.value); setCheckChosen(null); }} /></div>
|
|
120
|
+
</details>
|
|
121
|
+
</div>
|
|
122
|
+
<Button variant="outline" size="sm" loading={check.loading} disabled={!checkTarget} onClick={() => void check.mutate(checkTarget)}>Check</Button>
|
|
102
123
|
{check.data ? <p role="status" className="text-sm">{check.data.data.is_suppressed ? `Suppressed on: ${check.data.data.active_channels.join(", ")}` : "Not suppressed"}</p> : null}
|
|
103
124
|
{check.error ? <ApiError error={check.error} title="Could not check that contact" /> : null}
|
|
104
125
|
</section>
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* User flows:
|
|
11
11
|
* - filter the ledger by channel and reason, page through it, see how many are suppressed on each channel
|
|
12
|
-
* - Suppress contacts:
|
|
12
|
+
* - Suppress contacts: search and tick people (ContactPicker), or pick them from the CRM table, or paste ids under
|
|
13
|
+
* "Paste ids instead"; choose the channel (default all) and a reason
|
|
13
14
|
* - open a ledger row: the contact's record plus its live suppression status (read-only check)
|
|
14
|
-
* - check any contact
|
|
15
|
+
* - check any contact without opening it (pick them by name; no ids to type)
|
|
15
16
|
* - tick rows and Reinstate (confirmed: it lets sequences, campaigns and newsletters contact them again)
|
|
16
17
|
*
|
|
17
18
|
* SDK operations, in order:
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
|
|
25
26
|
import { useEffect, useState } from "react";
|
|
26
27
|
import type { ReactNode } from "react";
|
|
27
|
-
import { BadgeCheck, Ban,
|
|
28
|
+
import { BadgeCheck, Ban, ShieldOff, Undo2 } from "lucide-react";
|
|
28
29
|
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
29
30
|
import { ApiError } from "@/g8/components/api-error";
|
|
30
31
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -37,6 +38,8 @@ import { SectionCard } from "@/g8/components/screen/section-card";
|
|
|
37
38
|
import { SplitView } from "@/g8/components/screen/split-view";
|
|
38
39
|
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
39
40
|
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
41
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
42
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
40
43
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
41
44
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
42
45
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
@@ -85,6 +88,7 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
85
88
|
const full = view === "full";
|
|
86
89
|
const [adding, setAdding] = useState(false);
|
|
87
90
|
const [idsText, setIdsText] = useState("");
|
|
91
|
+
const [pickedContacts, setPickedContacts] = useState<ContactOption[]>([]);
|
|
88
92
|
const [addChannel, setAddChannel] = useState("all");
|
|
89
93
|
const [reason, setReason] = useState("");
|
|
90
94
|
const [picked, setPicked] = useState<Set<number>>(new Set());
|
|
@@ -92,8 +96,7 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
92
96
|
/** Set when Reinstate is started from a row's panel: the confirm acts on that contact only and leaves the ticked rows alone. */
|
|
93
97
|
const [panelTarget, setPanelTarget] = useState<number | null>(null);
|
|
94
98
|
const [reinstateReason, setReinstateReason] = useState("");
|
|
95
|
-
const [
|
|
96
|
-
const [checked, setChecked] = useState<number | null>(null);
|
|
99
|
+
const [checked, setChecked] = useState<ContactOption | null>(null);
|
|
97
100
|
|
|
98
101
|
const ledger = useG8Query((signal) => g8Get<SuppressionRecord[]>("/contacts/suppressions", { channel: channel || undefined, category: category || undefined, limit: pageSize, offset }, { signal }), [channel, category, offset, pageSize]);
|
|
99
102
|
const rows = (ledger.data?.data ?? []) as SuppressionRecord[];
|
|
@@ -101,13 +104,13 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
101
104
|
useEffect(() => { setPicked((p) => new Set([...p].filter((id) => rows.some((r) => r.contact_id === id)))); }, [ledger.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
102
105
|
|
|
103
106
|
const add = useG8Mutation((v: { ids: number[] }) => g8Post<BulkAdd>("/contacts/suppressions/bulk-add", { contact_ids: v.ids, channel: addChannel, reason: reason.trim() || undefined }), {
|
|
104
|
-
onSuccess: () => { setIdsText(""); setReason(""); void ledger.refetch(); },
|
|
107
|
+
onSuccess: () => { setIdsText(""); setPickedContacts([]); setReason(""); void ledger.refetch(); },
|
|
105
108
|
});
|
|
106
109
|
const reinstate = useG8Mutation((ids: number[]) => g8Post<Reinstate>("/contacts/suppressions/reinstate", { contact_ids: ids, reason: reinstateReason.trim() || undefined }), {
|
|
107
110
|
onSuccess: () => { setConfirmReinstate(false); setReinstateReason(""); if (panelTarget != null) { const done = panelTarget; setPicked((p) => { const n = new Set(p); n.delete(done); return n; }); } else setPicked(new Set()); setPanelTarget(null); void ledger.refetch(); },
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
const ids = parseIds(idsText);
|
|
113
|
+
const ids = [...new Set([...pickedContacts.map((c) => c.id), ...parseIds(idsText)])];
|
|
111
114
|
const chosen = panelTarget != null ? [panelTarget] : [...picked];
|
|
112
115
|
const togglePick = (id: number) => setPicked((p) => { const n = new Set(p); if (!n.delete(id)) n.add(id); return n; });
|
|
113
116
|
const byChannel = (c: string) => rows.filter((r) => r.channel === c).length;
|
|
@@ -205,9 +208,8 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
205
208
|
<select aria-label="Filter by reason" className={SELECT} value={category} onChange={(e) => { setCategory(e.target.value); resetPage(); }}>
|
|
206
209
|
<option value="">Any reason</option><option value="contact_initiated">Contact asked</option><option value="org_initiated">Our team</option><option value="system">System</option>
|
|
207
210
|
</select>
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
{checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked} contactId={checked} /><span className="text-xs text-muted-foreground">#{checked}</span></span> : null}
|
|
211
|
+
<ContactPicker label="Contact to check" placeholder="Check a contact" value={checked} onChange={setChecked} />
|
|
212
|
+
{checked ? <span className="flex items-center gap-1"><ContactStatusPanel key={checked.id} contactId={checked.id} /><span className="text-xs text-muted-foreground">{checked.name}</span></span> : null}
|
|
211
213
|
</>)}
|
|
212
214
|
actions={<Button variant="outline" size="sm" disabled={chosen.length === 0} onClick={() => { setPanelTarget(null); setConfirmReinstate(true); }}><BadgeCheck className="mr-1 size-4" aria-hidden />Reinstate {chosen.length || ""}</Button>}
|
|
213
215
|
/>
|
|
@@ -224,8 +226,15 @@ export function SuppressionsScreen({ pageSize = 50, toolbarActions, renderRowAct
|
|
|
224
226
|
<div className="flex min-w-40 flex-1 flex-col gap-1"><Label htmlFor="suppress-reason">Reason (optional)</Label><Input id="suppress-reason" value={reason} onChange={(e) => setReason(e.target.value)} /></div>
|
|
225
227
|
</div>
|
|
226
228
|
<div className="flex flex-col gap-2">
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
+
<span className="text-sm font-medium leading-none">Search and add people</span>
|
|
230
|
+
<ContactPicker multiple label="Contacts to suppress" selected={pickedContacts} onSelectedChange={setPickedContacts} />
|
|
231
|
+
<details className="text-sm">
|
|
232
|
+
<summary className="cursor-pointer text-muted-foreground">Paste ids instead</summary>
|
|
233
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
234
|
+
<Label htmlFor="suppress-ids">Contact ids</Label>
|
|
235
|
+
<Textarea id="suppress-ids" rows={3} placeholder="101, 102, 103" value={idsText} onChange={(e) => setIdsText(e.target.value)} />
|
|
236
|
+
</div>
|
|
237
|
+
</details>
|
|
229
238
|
<Button size="sm" loading={add.loading} disabled={ids.length === 0} onClick={() => void add.mutate({ ids })}>{ids.length ? `Suppress ${plural(ids.length, "contact")}` : "Suppress contacts"}</Button>
|
|
230
239
|
</div>
|
|
231
240
|
<div className="flex flex-col gap-2">
|
|
@@ -157,6 +157,10 @@
|
|
|
157
157
|
{
|
|
158
158
|
"file": "lib/mailboxes-utils.ts",
|
|
159
159
|
"purpose": "Mailbox and domain row types, credit estimate constants, status colors and idempotency-key helper."
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"file": "lib/use-provider-domains.ts",
|
|
163
|
+
"purpose": "Every sending domain, read page after page (the domains API pages) instead of one capped call."
|
|
160
164
|
}
|
|
161
165
|
],
|
|
162
166
|
"recipes": [
|