@graph8/sdk 0.362.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 +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +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 +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -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 +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- 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 +226 -0
- 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/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -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 +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- 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/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- 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 +25 -13
- 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 +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- 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 +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- 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 +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- 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 +56 -19
- 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 +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -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 +119 -0
- 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 +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- 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 +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- 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 +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- 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/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- 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 +39 -14
- 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/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- 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-list.tsx +2 -1
- 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 +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- 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 +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- 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 +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- 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 +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -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-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- 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/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/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 +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- 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 +147 -37
- 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 +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- 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 +98 -35
- 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 +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- 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/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- 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 +52 -20
- 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 +68 -34
- 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 +46 -0
- 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 +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -13,6 +13,7 @@ import { useState } from "react";
|
|
|
13
13
|
import { Radar } from "lucide-react";
|
|
14
14
|
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
15
15
|
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
16
17
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
17
18
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
19
|
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
@@ -42,8 +43,11 @@ export function AccountBriefTab({ companyId, name }: { companyId: number; name?:
|
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** The signals API has no paging: a full read is announced under the list. */
|
|
47
|
+
const SIGNAL_LIMIT = 20;
|
|
48
|
+
|
|
45
49
|
export function AccountSignalsTab({ companyId }: { companyId: number }) {
|
|
46
|
-
const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit:
|
|
50
|
+
const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: SIGNAL_LIMIT }, { signal }), [companyId]);
|
|
47
51
|
if (q.error) return <ApiError error={q.error} title="Could not load signals" onRetry={() => void q.refetch()} />;
|
|
48
52
|
if (q.loading && !q.data) return <Skeleton className="h-24 w-full" aria-label="Loading signals" />;
|
|
49
53
|
const payload = body(q.data);
|
|
@@ -51,6 +55,7 @@ export function AccountSignalsTab({ companyId }: { companyId: number }) {
|
|
|
51
55
|
const signals = raw.map(asRec);
|
|
52
56
|
if (signals.length === 0) return <EmptyState icon={<Radar className="size-8" />} title="Quiet" description="No recent buying or activity signals for this account." />;
|
|
53
57
|
return (
|
|
58
|
+
<>
|
|
54
59
|
<ul className="divide-y rounded-md border" aria-label="Signals">
|
|
55
60
|
{signals.map((s, i) => {
|
|
56
61
|
const when = text(s.detected_at ?? s.created_at ?? s.occurred_at);
|
|
@@ -62,5 +67,7 @@ export function AccountSignalsTab({ companyId }: { companyId: number }) {
|
|
|
62
67
|
);
|
|
63
68
|
})}
|
|
64
69
|
</ul>
|
|
70
|
+
<FirstNotice shown={signals.length} limit={SIGNAL_LIMIT} noun="signals" />
|
|
71
|
+
</>
|
|
65
72
|
);
|
|
66
73
|
}
|
|
@@ -35,15 +35,18 @@ export interface ChurnListProps {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** Loaded on click: one call per row up front would be 25 calls for a page the person may only skim. */
|
|
38
|
+
/** The signals API has no paging: a full read is shown as "20+ recent signals". */
|
|
39
|
+
const SIGNAL_LIMIT = 20;
|
|
40
|
+
|
|
38
41
|
function Intent({ companyId }: { companyId: number }) {
|
|
39
42
|
const [asked, setAsked] = useState(false);
|
|
40
|
-
const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit:
|
|
43
|
+
const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: SIGNAL_LIMIT }, { signal }), [companyId], { enabled: asked });
|
|
41
44
|
if (!asked) return <Button size="sm" variant="ghost" aria-label={`Check activity for ${companyId}`} onClick={() => setAsked(true)}>Check</Button>;
|
|
42
45
|
if (q.loading) return <Skeleton className="h-4 w-16" />;
|
|
43
46
|
if (q.error) return <span className="text-muted-foreground text-xs">n/a</span>;
|
|
44
47
|
const payload = body(q.data);
|
|
45
48
|
const signals = Array.isArray(payload) ? payload : Array.isArray(asRec(payload).signals) ? (asRec(payload).signals as unknown[]) : [];
|
|
46
|
-
return <span className="text-xs">{signals.length ? `${signals.length} recent signals` : "Quiet"}</span>;
|
|
49
|
+
return <span className="text-xs">{signals.length ? `${signals.length}${signals.length >= SIGNAL_LIMIT ? "+" : ""} recent signals` : "Quiet"}</span>;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export function ChurnList({ stageSlug = "churned", winBackSlug = "leads", onOpenCompany, pageSize = 25, className }: ChurnListProps) {
|
|
@@ -41,7 +41,7 @@ function Summary() {
|
|
|
41
41
|
return (
|
|
42
42
|
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4" data-testid="revenue-summary">
|
|
43
43
|
{tiles.map(([key, value]) => (
|
|
44
|
-
<Card key={key}><CardContent className="p-4"><div className="text-2xl font-
|
|
44
|
+
<Card key={key}><CardContent className="p-4"><div className="text-2xl font-medium">{value.toLocaleString()}</div><div className="text-muted-foreground text-xs capitalize">{key.replace(/_count$/, "").replace(/_/g, " ")}</div></CardContent></Card>
|
|
45
45
|
))}
|
|
46
46
|
</div>
|
|
47
47
|
);
|
|
@@ -25,6 +25,7 @@ import { useLifecycleStages } from "../hooks/use-lifecycle-stages";
|
|
|
25
25
|
import { asRec, body, money } from "../lib/lifecycle";
|
|
26
26
|
import type { CompanyRecord, LifecycleStage } from "../lib/lifecycle";
|
|
27
27
|
import { StageMoveDialog } from "./stage-move-dialog";
|
|
28
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
28
29
|
|
|
29
30
|
export interface LifecycleBoardProps {
|
|
30
31
|
/** Open a company in your detail route (CompanyDetail from the crm block, for example). */
|
|
@@ -77,13 +78,14 @@ function Column({ stage, count, pageSize, version, onOpenCompany, onMove }: Colu
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export function LifecycleBoard({ onOpenCompany, pageSize = 25, className }: LifecycleBoardProps) {
|
|
81
|
+
const brand = useBrand();
|
|
80
82
|
const { stages, countOf, loading, error, refetch } = useLifecycleStages();
|
|
81
83
|
const [moving, setMoving] = useState<{ record: CompanyRecord; stage: LifecycleStage } | null>(null);
|
|
82
84
|
const [version, setVersion] = useState(0);
|
|
83
85
|
|
|
84
86
|
if (loading) return <div className={className} aria-busy="true"><Skeleton className="h-48 w-full" /></div>;
|
|
85
87
|
if (error) return <ApiError error={error} onRetry={() => void refetch()} />;
|
|
86
|
-
if (stages.length === 0) return <EmptyState title="No lifecycle stages" description=
|
|
88
|
+
if (stages.length === 0) return <EmptyState title="No lifecycle stages" description={`Stages are configured in ${brand.linkLabel} under Revenue settings.`} />;
|
|
87
89
|
|
|
88
90
|
return (
|
|
89
91
|
<div className={className}>
|
|
@@ -47,6 +47,7 @@ import { CustomerTable } from "./customer-table";
|
|
|
47
47
|
import { LeadQueue } from "./lead-queue";
|
|
48
48
|
import { LifecycleBoard } from "./lifecycle-board";
|
|
49
49
|
import { StageMoveDialog } from "./stage-move-dialog";
|
|
50
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
50
51
|
|
|
51
52
|
export interface LifecycleScreenProps {
|
|
52
53
|
/** Buttons next to "Move stage" in the record panel. */
|
|
@@ -78,6 +79,7 @@ function StageTab({ stage, version, onOpenCompany, onOpenContact }: { stage: Lif
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
export function LifecycleScreen({ renderDetailActions, extraDetailTabs, toolbarActions }: LifecycleScreenProps) {
|
|
82
|
+
const brand = useBrand();
|
|
81
83
|
const { stages, countOf, loading, error, refetch } = useLifecycleStages();
|
|
82
84
|
const [tabParam, setTab] = useUrlState("tab", "board");
|
|
83
85
|
const [raw, setRaw] = useUrlState("rec");
|
|
@@ -123,7 +125,7 @@ export function LifecycleScreen({ renderDetailActions, extraDetailTabs, toolbarA
|
|
|
123
125
|
|
|
124
126
|
let body: ReactNode;
|
|
125
127
|
if (error) body = <ApiError error={error} title="Could not load lifecycle stages" onRetry={() => void refetch()} />;
|
|
126
|
-
else if (!loading && stages.length === 0) body = <EmptyState title="No lifecycle stages" description=
|
|
128
|
+
else if (!loading && stages.length === 0) body = <EmptyState title="No lifecycle stages" description={`Stages are configured in ${brand.linkLabel} under Revenue settings.`} />;
|
|
127
129
|
else if (activeStage) body = <StageTab stage={activeStage} version={version} onOpenCompany={openCompany} onOpenContact={openContact} />;
|
|
128
130
|
else body = <LifecycleBoard key={version} onOpenCompany={openCompany} />;
|
|
129
131
|
|
|
@@ -85,7 +85,7 @@ export function money(cents: number | null | undefined, currency = "USD"): strin
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function contactName(c: ContactRecord): string {
|
|
88
|
-
return [c.first_name, c.last_name].filter(Boolean).join(" ") ||
|
|
88
|
+
return [c.first_name, c.last_name].filter(Boolean).join(" ") || (c.id != null ? `#${c.id}` : "-");
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** A masked field arrives as `"***"`; the real value needs an unlock (billable) or an enrichment. */
|
package/blocks/lists/README.md
CHANGED
|
@@ -6,6 +6,19 @@ It builds on the `crm` block (the members panel is a `ContactTable`).
|
|
|
6
6
|
|
|
7
7
|
Install: `npx @graph8/sdk add lists` (installs `foundation` and `crm` first).
|
|
8
8
|
|
|
9
|
+
## Screens
|
|
10
|
+
|
|
11
|
+
Two finished pages. Mount one as a route with no props (`<ListsScreen />`); both take optional `toolbarActions`,
|
|
12
|
+
`renderRowActions` and `renderDetailActions` slots for cross-block glue.
|
|
13
|
+
|
|
14
|
+
| Screen | What it does |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `ListsScreen` | Searchable, paged table of lists with member counts and build status; **New list** / Edit / Delete (confirmed) in a side sheet; open a list (`?id=`) for its Members tab (contacts, remove from list with confirmation), **Add contacts** from the CRM (conflicts shown, never auto-picked), **Refresh** for dynamic lists, **Check health**, and a Details tab. **Export** downloads a list as CSV or XLSX. `?view=full` opens a list or a suppressed contact full-page. Company lists show counts; the API has no read operation for company-list members, so they link out to the graph8 app. |
|
|
17
|
+
| `SuppressionsScreen` | Do-not-contact ledger filtered by channel and reason; **Suppress contacts** by id or by ticking people from the CRM (default channel: all); check any contact id; open a row for the contact's record and live status; **Reinstate** (confirmed, lifts every suppression on the contact). |
|
|
18
|
+
|
|
19
|
+
Prerequisites: `crm` installed (the CLI does it), proxy allowlist `lists` and `contacts`, key scopes `lists:read/write/delete`
|
|
20
|
+
and `contacts:read/write`. The screens use the foundation screen kit (`components/screen/*`, `hooks/use-url-state`).
|
|
21
|
+
|
|
9
22
|
## What you get
|
|
10
23
|
|
|
11
24
|
| Component | Use it for | Embed props |
|
|
@@ -26,7 +39,7 @@ Install: `npx @graph8/sdk add lists` (installs `foundation` and `crm` first).
|
|
|
26
39
|
shows `ready_count / warned / blocked` and retries with `conflict_resolution: "skip_all" | "add_all"`.
|
|
27
40
|
4. **Members**: `ContactTable listId={id}` (or `GET /contacts?list_id=`). Removing uses
|
|
28
41
|
`DELETE /lists/{list_id}/contacts { contact_ids }`; contacts are never deleted by it.
|
|
29
|
-
5. **Suppress**: `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
|
|
42
|
+
5. **Suppress**: pick people by name with the `ContactPicker` (pickers block) (no ids to type; pasting ids stays under "Paste ids instead"), then `POST /contacts/suppressions/bulk-add { contact_ids, channel, reason }`; read back with
|
|
30
43
|
`GET /contacts/suppressions`. The ledger has ids only (no names or emails): open the contact for details.
|
|
31
44
|
6. **Reinstate**: `POST /contacts/suppressions/reinstate { contact_ids }`. Contacts who unsubscribed themselves
|
|
32
45
|
can be refused; the answer lists them in `skipped`.
|
|
@@ -94,3 +107,9 @@ Ask the person: who may reinstate suppressed contacts (decide it in the proxy's
|
|
|
94
107
|
| checkContactSuppression | GET /contacts/{contact_id}/suppression | read | SuppressionManager |
|
|
95
108
|
| reinstateSuppressions | POST /contacts/suppressions/reinstate | write | SuppressionManager |
|
|
96
109
|
| refreshDynamicList, checkListIntegrity | POST /lists/{id}/refresh, /lists/{id}/integrity-check | write, read | recipes |
|
|
110
|
+
|
|
111
|
+
## Cross-block glue
|
|
112
|
+
|
|
113
|
+
- `AddCompanyPeopleToListButton` (`components/add-company-people-to-list-button.tsx`): "Add people to list" for companies. Lists hold contacts, so it reads the people at the companies (`GET /contacts?company_id=`, up to 500) and opens `AddToListButton` for them. `AddToListButton` gains optional `defaultOpen`, `hideTrigger` and `onClosed`; `ListsScreen` gains optional `onOpenContact` so list members open in your contacts screen.
|
|
114
|
+
|
|
115
|
+
- `RecordListsTab` (`components/record-lists-tab.tsx`): the lists one contact belongs to (`getContactLists`), with `AddToListButton` and a confirmed Remove (`removeContactsFromList`, destructive). `extraDetailTabs` entry for ContactsScreen: `{ id: "lists", label: "Lists", render: (c) => <RecordListsTab contactId={c.id} onOpenList={(id) => router.push(`/lists?id=${id}`)} /> }`.
|
package/blocks/lists/block.json
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"members panel built on the CRM ContactTable with remove-from-list",
|
|
9
9
|
"ListPicker (search or create) and AddToListButton for any contact selection",
|
|
10
10
|
"conflict review when contacts belong to other reps",
|
|
11
|
-
"suppression ledger by channel and reason, bulk suppress, check, reinstate"
|
|
11
|
+
"suppression ledger by channel and reason, bulk suppress, check, reinstate",
|
|
12
|
+
"finished ListsScreen and SuppressionsScreen pages"
|
|
12
13
|
],
|
|
13
14
|
"dependsOn": [
|
|
14
|
-
"crm"
|
|
15
|
+
"crm",
|
|
16
|
+
"pickers"
|
|
15
17
|
],
|
|
16
18
|
"operations": [
|
|
17
19
|
"add_contacts_to_list_lists__list_id__contacts_post",
|
|
@@ -20,6 +22,8 @@
|
|
|
20
22
|
"check_list_integrity_lists__list_id__integrity_check_post",
|
|
21
23
|
"create_list_lists_post",
|
|
22
24
|
"delete_list_lists__list_id__delete",
|
|
25
|
+
"download_list_lists__audience_id__download_get",
|
|
26
|
+
"get_contact_lists_contacts__contact_id__lists_get",
|
|
23
27
|
"list_contact_fields_fields_get",
|
|
24
28
|
"list_contacts_contacts_get",
|
|
25
29
|
"list_lists_lists_get",
|
|
@@ -84,12 +88,73 @@
|
|
|
84
88
|
"list_suppressions_contacts_suppressions_get",
|
|
85
89
|
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
86
90
|
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
87
|
-
"reinstate_suppressions_contacts_suppressions_reinstate_post"
|
|
91
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post",
|
|
92
|
+
"list_contacts_contacts_get"
|
|
88
93
|
],
|
|
89
94
|
"tiers": [
|
|
90
95
|
"read",
|
|
91
96
|
"write"
|
|
92
97
|
]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "ListsScreen",
|
|
101
|
+
"file": "components/lists-screen.tsx",
|
|
102
|
+
"purpose": "Finished Lists page: searchable table, create/edit/delete, member panel with add and remove, refresh and health check.",
|
|
103
|
+
"operations": [
|
|
104
|
+
"check_list_integrity_lists__list_id__integrity_check_post",
|
|
105
|
+
"create_list_lists_post",
|
|
106
|
+
"delete_list_lists__list_id__delete",
|
|
107
|
+
"download_list_lists__audience_id__download_get",
|
|
108
|
+
"list_lists_lists_get",
|
|
109
|
+
"refresh_dynamic_list_lists__audience_id__refresh_post",
|
|
110
|
+
"remove_contacts_from_list_lists__list_id__contacts_delete",
|
|
111
|
+
"update_list_lists__list_id__patch"
|
|
112
|
+
],
|
|
113
|
+
"tiers": [
|
|
114
|
+
"read",
|
|
115
|
+
"write",
|
|
116
|
+
"destructive"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "SuppressionsScreen",
|
|
121
|
+
"file": "components/suppressions-screen.tsx",
|
|
122
|
+
"purpose": "Finished do-not-contact page: filtered ledger, suppress by id or from the CRM, contact detail, confirmed reinstate.",
|
|
123
|
+
"operations": [
|
|
124
|
+
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
125
|
+
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
126
|
+
"list_suppressions_contacts_suppressions_get",
|
|
127
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post",
|
|
128
|
+
"list_contacts_contacts_get"
|
|
129
|
+
],
|
|
130
|
+
"tiers": [
|
|
131
|
+
"read",
|
|
132
|
+
"write"
|
|
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
|
+
]
|
|
93
158
|
}
|
|
94
159
|
],
|
|
95
160
|
"hooks": [],
|
|
@@ -161,6 +226,7 @@
|
|
|
161
226
|
}
|
|
162
227
|
],
|
|
163
228
|
"peerDependencies": {
|
|
229
|
+
"lucide-react": ">=0.400.0",
|
|
164
230
|
"react": ">=18"
|
|
165
231
|
}
|
|
166
232
|
}
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,7 @@ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogH
|
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
23
|
import { g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { plural } from "@/g8/lib/plural";
|
|
24
25
|
import { ListPicker } from "./list-picker";
|
|
25
26
|
import type { ListSummary } from "./list-picker";
|
|
26
27
|
|
|
@@ -41,6 +42,12 @@ export interface AddToListButtonProps {
|
|
|
41
42
|
disabled?: boolean;
|
|
42
43
|
onAdded?: (result: AddToListResult) => void;
|
|
43
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;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
interface ConflictDetail { code?: string; ready_count?: number; warned?: unknown[]; blocked?: unknown[]; message?: string }
|
|
@@ -51,30 +58,34 @@ function conflictOf(error: unknown): ConflictDetail | null {
|
|
|
51
58
|
return e?.status === 409 && e.detail && typeof e.detail === "object" && e.detail.code === "CONFLICT_REVIEW_REQUIRED" ? e.detail : null;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export function AddToListButton({ contactIds, listId, label = "Add to list", variant = "outline", size = "sm", disabled, onAdded, className }: AddToListButtonProps) {
|
|
55
|
-
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);
|
|
56
63
|
const [target, setTarget] = useState<ListSummary | null>(null);
|
|
57
64
|
const [result, setResult] = useState<AddToListResult | null>(null);
|
|
58
65
|
const destination = listId ?? target?.id;
|
|
59
66
|
|
|
60
67
|
const add = useG8Mutation(async (resolution?: Resolution) => {
|
|
61
|
-
const res = await g8Post<{ added
|
|
68
|
+
const res = await g8Post<{ added?: number; already_member?: number[]; not_found?: number[] }>(`/lists/${destination}/contacts`, {
|
|
62
69
|
contact_ids: contactIds,
|
|
63
70
|
...(resolution ? { conflict_resolution: resolution } : {}),
|
|
64
71
|
});
|
|
65
|
-
|
|
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 };
|
|
77
|
+
}, { invalidates: [["lists"], ["contacts", "list"]], onSuccess: (r) => { setResult(r); onAdded?.(r); } });
|
|
67
78
|
|
|
68
79
|
const conflict = conflictOf(add.error);
|
|
69
|
-
const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); };
|
|
80
|
+
const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); onClosed?.(); };
|
|
70
81
|
|
|
71
82
|
return (
|
|
72
83
|
<>
|
|
73
|
-
<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>}
|
|
74
85
|
<Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
|
75
86
|
<DialogContent>
|
|
76
87
|
<DialogHeader>
|
|
77
|
-
<DialogTitle>Add {
|
|
88
|
+
<DialogTitle>Add {plural(contactIds.length, "contact")} to a list</DialogTitle>
|
|
78
89
|
<DialogDescription>Contacts already in the list are skipped.</DialogDescription>
|
|
79
90
|
</DialogHeader>
|
|
80
91
|
|
|
@@ -88,7 +99,7 @@ export function AddToListButton({ contactIds, listId, label = "Add to list", var
|
|
|
88
99
|
<>
|
|
89
100
|
{listId == null ? <ListPicker value={target?.id} onChange={(l) => { setTarget(l); add.reset(); }} /> : null}
|
|
90
101
|
{conflict ? (
|
|
91
|
-
<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">
|
|
92
103
|
<p>{conflict.message ?? "Some contacts belong to other reps."}</p>
|
|
93
104
|
<p className="mt-1">{conflict.ready_count ?? 0} ready, {conflict.warned?.length ?? 0} flagged, {conflict.blocked?.length ?? 0} blocked (never added).</p>
|
|
94
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 {
|
|
@@ -31,6 +33,9 @@ export interface ListSummary {
|
|
|
31
33
|
contact_count?: number | null;
|
|
32
34
|
company_count?: number | null;
|
|
33
35
|
is_dynamic?: boolean;
|
|
36
|
+
source?: string | null;
|
|
37
|
+
created_by?: string | null;
|
|
38
|
+
tags?: string[];
|
|
34
39
|
created_at?: string | null;
|
|
35
40
|
updated_at?: string | null;
|
|
36
41
|
}
|
|
@@ -56,16 +61,24 @@ function useDebounced<T>(value: T, ms = 250): T {
|
|
|
56
61
|
return v;
|
|
57
62
|
}
|
|
58
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
|
+
|
|
59
67
|
export function ListPicker({ value, onChange, type = "contacts", allowCreate = true, suggestedName, className }: ListPickerProps) {
|
|
60
68
|
const [search, setSearch] = useState("");
|
|
61
69
|
const [newName, setNewName] = useState(suggestedName ?? "");
|
|
62
70
|
const q = useDebounced(search.trim());
|
|
63
|
-
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]);
|
|
64
76
|
const create = useG8Mutation((title: string) => g8Post<ListSummary>("/lists", { title, type }), {
|
|
77
|
+
invalidates: [["lists"]],
|
|
65
78
|
onSuccess: (res) => { setNewName(""); void lists.refetch(); onChange({ type, ...res.data, title: res.data.title ?? newName }); },
|
|
66
79
|
});
|
|
67
80
|
|
|
68
|
-
const rows = (
|
|
81
|
+
const rows = (lists.data ?? []).filter((l) => !l.type || l.type === type);
|
|
69
82
|
return (
|
|
70
83
|
<div className={`flex flex-col gap-2 ${className ?? ""}`}>
|
|
71
84
|
<Input aria-label="Search lists" placeholder="Search lists" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
@@ -82,6 +95,7 @@ export function ListPicker({ value, onChange, type = "contacts", allowCreate = t
|
|
|
82
95
|
</li>
|
|
83
96
|
))}
|
|
84
97
|
</ul>
|
|
98
|
+
<LoadMore shown={rows.length} noun="lists" hasMore={lists.hasMore} loading={lists.loadingMore} onLoadMore={lists.loadMore} />
|
|
85
99
|
{allowCreate ? (
|
|
86
100
|
<form className="flex items-center gap-2" onSubmit={(e) => { e.preventDefault(); if (newName.trim()) void create.mutate(newName.trim()); }}>
|
|
87
101
|
<Input aria-label="New list name" placeholder="Or create a new list" value={newName} onChange={(e) => setNewName(e.target.value)} />
|
|
@@ -27,9 +27,13 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
|
27
27
|
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
28
28
|
import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
|
|
29
29
|
import { usePageQuery } from "@/g8/crm/hooks/use-page-query";
|
|
30
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
31
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
30
32
|
import type { ListSummary } from "./list-picker";
|
|
31
33
|
|
|
32
34
|
export interface ListTableProps {
|
|
35
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
36
|
+
linkParam?: string;
|
|
33
37
|
pageSize?: number;
|
|
34
38
|
/** Called when a list row is clicked. Without it, contact lists open the members panel. */
|
|
35
39
|
onOpen?: (list: ListSummary) => void;
|
|
@@ -42,9 +46,9 @@ function count(l: ListSummary): number {
|
|
|
42
46
|
return l.contact_count ?? l.company_count ?? l.total ?? 0;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
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" };
|
|
46
50
|
|
|
47
|
-
export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
|
|
51
|
+
export function ListTable({ linkParam, pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
|
|
48
52
|
const [search, setSearch] = useState("");
|
|
49
53
|
const debouncedSearch = useDebouncedValue(search);
|
|
50
54
|
const [editing, setEditing] = useState<{ id?: number; title: string; description: string; type: "contacts" | "companies" } | null>(null);
|
|
@@ -57,13 +61,15 @@ export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKe
|
|
|
57
61
|
(page, signal) => g8Get<ListSummary[]>("/lists", { page, limit: pageSize, search: debouncedSearch.trim() || undefined }, { signal }),
|
|
58
62
|
[debouncedSearch, refreshKey],
|
|
59
63
|
pageSize,
|
|
64
|
+
["lists", "list", "table"],
|
|
60
65
|
);
|
|
61
66
|
const save = useG8Mutation(
|
|
62
67
|
(v: NonNullable<typeof editing>) => (v.id ? g8Patch(`/lists/${v.id}`, { title: v.title, description: v.description }) : g8Post("/lists", { title: v.title, type: v.type, description: v.description || undefined })),
|
|
63
|
-
{ onSuccess: () => { setEditing(null); void lists.refetch(); } },
|
|
68
|
+
{ invalidates: [["lists"]], onSuccess: () => { setEditing(null); void lists.refetch(); } },
|
|
64
69
|
);
|
|
65
|
-
const remove = useG8Mutation((id: number) => g8Delete(`/lists/${id}`), { onSuccess: () => { setDeleting(null); void lists.refetch(); } });
|
|
70
|
+
const remove = useG8Mutation((id: number) => g8Delete(`/lists/${id}`), { invalidates: [["lists"]], onSuccess: () => { setDeleting(null); void lists.refetch(); } });
|
|
66
71
|
const removeMembers = useG8Mutation((v: { listId: number; ids: number[] }) => g8Delete(`/lists/${v.listId}/contacts`, { body: { contact_ids: v.ids } }), {
|
|
72
|
+
invalidates: [["lists"], ["contacts", "list"]],
|
|
67
73
|
onSuccess: () => { setRemoving(null); setMembersKey((k) => k + 1); void lists.refetch(); },
|
|
68
74
|
});
|
|
69
75
|
|
|
@@ -87,8 +93,8 @@ export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKe
|
|
|
87
93
|
{lists.loading && lists.rows.length === 0
|
|
88
94
|
? Array.from({ length: 4 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={6}><Skeleton className="h-5 w-full" /></TableCell></TableRow>)
|
|
89
95
|
: lists.rows.map((l) => (
|
|
90
|
-
<TableRow key={l.id} data-testid={`list-row-${l.id}`} className="cursor-pointer"
|
|
91
|
-
<TableCell className="font-medium">{l.title}{l.is_dynamic ? <span className="ml-2 text-xs text-muted-foreground">dynamic</span> : null}</TableCell>
|
|
96
|
+
<TableRow key={l.id} data-testid={`list-row-${l.id}`} className="cursor-pointer" {...rowActivationProps(() => open(l))}>
|
|
97
|
+
<TableCell className="font-medium">{onOpen || l.type !== "companies" ? <RecordLink param={linkParam} id={l.id} onOpen={() => open(l)}>{l.title}</RecordLink> : l.title}{l.is_dynamic ? <span className="ml-2 text-xs text-muted-foreground">dynamic</span> : null}</TableCell>
|
|
92
98
|
<TableCell className="capitalize">{l.type ?? "-"}</TableCell>
|
|
93
99
|
<TableCell className="text-right">{count(l).toLocaleString()}</TableCell>
|
|
94
100
|
<TableCell className={STATUS_STYLE[l.status ?? ""] ?? ""} title={l.status_detail ?? undefined}>{l.status_reason ? l.status_reason.replace(/_/g, " ") : l.status ?? "-"}</TableCell>
|