@graph8/sdk 0.370.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* or many (confirm, "N of M" report, 404 = already gone) - import a CSV/Excel file - review and merge duplicates -
|
|
13
13
|
* add notes and tasks - hand a contact to other blocks through the slot props.
|
|
14
14
|
*
|
|
15
|
+
* Record actions: assign an owner to a whole list (only with renderOwnerPicker, never for a selection), tag chips on the open
|
|
16
|
+
* contact plus add/remove tags on a selection and a tag manager, edit one field on a selection, verify emails and unlock
|
|
17
|
+
* locked contacts (both billable, behind ConfirmSpendDialog with the count taken from the selection), AI documents and
|
|
18
|
+
* signals/next best step tabs on the open contact. Each action also works from the row's "..." button.
|
|
19
|
+
*
|
|
15
20
|
* SDK operations: none of its own. listContacts, getContact, getActivityFeed, duplicateCounts are `read`;
|
|
16
21
|
* the forms, notes and tasks are `write`; deleteContact (rows, detail and bulk) is `destructive` and always goes
|
|
17
22
|
* through ConfirmSpendDialog. Nothing here sends a message to a person.
|
|
@@ -22,7 +27,7 @@
|
|
|
22
27
|
import { useEffect, useMemo, useState } from "react";
|
|
23
28
|
import type { ReactNode } from "react";
|
|
24
29
|
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
25
|
-
import { Building2, GitMerge, Lock, Plus, Trash2, Upload, Users } from "lucide-react";
|
|
30
|
+
import { Building2, GitMerge, Lock, Plus, Tags, Trash2, Upload, Users } from "lucide-react";
|
|
26
31
|
import { ApiError } from "@/g8/components/api-error";
|
|
27
32
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
28
33
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
@@ -40,17 +45,30 @@ import { RecordLink } from "@/g8/components/screen/record-link";
|
|
|
40
45
|
import { plural } from "@/g8/lib/plural";
|
|
41
46
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
42
47
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
48
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
49
|
+
import { ListSelect } from "./list-select";
|
|
50
|
+
import { MoreFilters, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
43
51
|
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
44
|
-
import { useDebouncedValue } from "../hooks/use-debounced";
|
|
45
52
|
import { usePageQuery } from "../hooks/use-page-query";
|
|
46
|
-
import { contactName, errorStatus, isMasked, location } from "../lib/crm-types";
|
|
53
|
+
import { contactName, contactSubtitle, errorStatus, isMasked, location } from "../lib/crm-types";
|
|
47
54
|
import type { ContactRecord, ScreenActionContext } from "../lib/crm-types";
|
|
48
55
|
import { Pager, SortHead, formatDay, sortRows, useBulkDelete, useRowSelection, useSort } from "../lib/screen-parts";
|
|
49
56
|
import { ActivityTimeline } from "./activity-timeline";
|
|
50
57
|
import { DedupeReview } from "./dedupe-review";
|
|
51
58
|
import { ImportWizard } from "./import-wizard";
|
|
52
59
|
import { NotesPanel } from "./notes-panel";
|
|
60
|
+
import { OwnerAssignListButton } from "./owner-assign";
|
|
61
|
+
import type { OwnerPickerProps } from "./owner-assign";
|
|
53
62
|
import { RecordForm } from "./record-form";
|
|
63
|
+
import { BulkUpdateButton } from "./bulk-update";
|
|
64
|
+
import { ContactDocumentsTab } from "./contact-documents-tab";
|
|
65
|
+
import { ContactInsightsTab } from "./contact-insights-tab";
|
|
66
|
+
import { UnlockInfoButton } from "./unlock-info-button";
|
|
67
|
+
import { VerifyEmailButton } from "./verify-email-button";
|
|
68
|
+
import { ContactRowActions } from "./contact-row-actions";
|
|
69
|
+
import { ContactTagChips } from "./contact-tag-chips";
|
|
70
|
+
import { TagAssignButton } from "./tag-assign";
|
|
71
|
+
import { TagManager } from "./tag-manager";
|
|
54
72
|
import { TaskList } from "./task-list";
|
|
55
73
|
|
|
56
74
|
export interface ContactsScreenProps {
|
|
@@ -67,13 +85,22 @@ export interface ContactsScreenProps {
|
|
|
67
85
|
toolbarActions?: ReactNode;
|
|
68
86
|
/** Extra bulk actions next to Delete (add to list, enrich, enroll...). */
|
|
69
87
|
renderBulkActions?: (selection: { ids: number[]; rows: ContactRecord[]; clear: () => void }) => ReactNode;
|
|
88
|
+
/**
|
|
89
|
+
* Team-member picker for "Assign owner to all N in this list" (pass the pickers block's MemberPicker:
|
|
90
|
+
* `renderOwnerPicker={(p) => <MemberPicker label="New owner" {...p} />}`). Injected so crm does not depend on pickers.
|
|
91
|
+
* Without it the action is hidden. It is offered only when a list is in scope with no other filter, and never for a selection:
|
|
92
|
+
* the API assigns owners by filter, not by id.
|
|
93
|
+
*/
|
|
94
|
+
renderOwnerPicker?: (props: OwnerPickerProps) => ReactNode;
|
|
70
95
|
/** Called with a company id from the contact panel ("Open company"). Without it the button is hidden. */
|
|
71
96
|
onOpenCompany?: (companyId: number) => void;
|
|
72
97
|
className?: string;
|
|
73
98
|
}
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
const
|
|
100
|
+
/** Every filter the contacts API takes (name, email, job title, seniority, company name and id, country, state, city, department, industry), plus `list`. */
|
|
101
|
+
const FILTER_KEYS = ["name", "job_title", "seniority_level", "country", "email", "company_name", "company_id", "state", "city", "job_department", "industry", "list"] as const;
|
|
102
|
+
type FilterKey = (typeof FILTER_KEYS)[number];
|
|
103
|
+
const MORE_KEYS: readonly FilterKey[] = ["email", "company_name", "company_id", "state", "city", "job_department", "industry", "list"];
|
|
77
104
|
const SENIORITY = ["C-Suite", "VP", "Director", "Manager"];
|
|
78
105
|
|
|
79
106
|
function sortValue(row: ContactRecord, key: string): string | null | undefined {
|
|
@@ -81,6 +108,7 @@ function sortValue(row: ContactRecord, key: string): string | null | undefined {
|
|
|
81
108
|
if (key === "title") return row.job_title;
|
|
82
109
|
if (key === "company") return row.company_name;
|
|
83
110
|
if (key === "location") return location(row);
|
|
111
|
+
if (key === "owner") return row.owner_name;
|
|
84
112
|
return null;
|
|
85
113
|
}
|
|
86
114
|
|
|
@@ -101,23 +129,28 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
101
129
|
const parsedId = idParam ? Number(idParam) : null;
|
|
102
130
|
/** A malformed deep link (`?id=abc`) opens nothing instead of fetching /contacts/NaN. */
|
|
103
131
|
const openId = parsedId != null && Number.isFinite(parsedId) ? parsedId : null;
|
|
104
|
-
|
|
105
|
-
const
|
|
132
|
+
// The filters live in the URL (`c_email=...`), so a filtered view survives reload, Back and sharing.
|
|
133
|
+
const urlFilters = useUrlFilters(FILTER_KEYS, { prefix: "c_" });
|
|
134
|
+
const { draft, filters } = urlFilters;
|
|
135
|
+
const apiFilters = Object.fromEntries(Object.entries(filters).filter(([k, v]) => v && k !== "list"));
|
|
136
|
+
const effectiveListId = listId ?? (filters.list ? Number(filters.list) : undefined);
|
|
106
137
|
const [sheet, setSheet] = useState<"new" | "import" | "dupes" | null>(null);
|
|
107
138
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
139
|
+
const [tagManager, setTagManager] = useState(false);
|
|
108
140
|
const [note, setNote] = useState<string | null>(null);
|
|
109
141
|
const { sort, toggle: toggleSort } = useSort();
|
|
110
142
|
|
|
111
143
|
const list = usePageQuery<ContactRecord>(
|
|
112
|
-
(page, signal) => g8Get<ContactRecord[]>("/contacts", { page, limit: pageSize, list_id:
|
|
113
|
-
[
|
|
144
|
+
(page, signal) => g8Get<ContactRecord[]>("/contacts", { page, limit: pageSize, list_id: effectiveListId, ...apiFilters }, { signal }),
|
|
145
|
+
[effectiveListId, filters],
|
|
114
146
|
pageSize,
|
|
115
147
|
["contacts", "list", "screen"],
|
|
148
|
+
urlFilters.ready,
|
|
116
149
|
);
|
|
117
150
|
const dupes = useG8Query((signal) => g8Get<{ pending?: number; count?: number }>("/duplicates/counts", { entity_type: "contact" }, { signal }), [], { key: ["duplicates", "counts", "contact"] });
|
|
118
151
|
const selection = useRowSelection<ContactRecord>(`${listId}|${JSON.stringify(filters)}`);
|
|
119
152
|
const rows = useMemo(() => sortRows(list.rows, sort, sortValue), [list.rows, sort]);
|
|
120
|
-
const hasFilters =
|
|
153
|
+
const hasFilters = urlFilters.active;
|
|
121
154
|
const pending = dupes.data?.data.pending ?? dupes.data?.data.count;
|
|
122
155
|
const lockedOnPage = list.rows.filter((r) => isMasked(r.work_email)).length;
|
|
123
156
|
|
|
@@ -129,9 +162,11 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
129
162
|
void list.refetch();
|
|
130
163
|
void dupes.refetch();
|
|
131
164
|
});
|
|
165
|
+
/** The freshest copy of each selected row (a refetch after unlock or owner change replaces the stored one). */
|
|
166
|
+
const selectedRows = selection.ids.map((id) => list.rows.find((r) => r.id === id) ?? selection.selected.get(id)!).filter(Boolean);
|
|
132
167
|
const allOnPage = rows.length > 0 && rows.every((r) => selection.selected.has(r.id));
|
|
133
168
|
const reload = () => { void list.refetch(); void dupes.refetch(); };
|
|
134
|
-
const set = (key:
|
|
169
|
+
const set = (key: FilterKey, v: string) => urlFilters.set(key, v);
|
|
135
170
|
|
|
136
171
|
useEffect(() => { setNote(null); }, [filters, listId]);
|
|
137
172
|
|
|
@@ -144,6 +179,8 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
144
179
|
actions={
|
|
145
180
|
<>
|
|
146
181
|
{props.toolbarActions}
|
|
182
|
+
{effectiveListId != null && Object.keys(apiFilters).length === 0 && props.renderOwnerPicker ? <OwnerAssignListButton renderPicker={props.renderOwnerPicker} listId={effectiveListId} total={list.loading ? null : list.total} onDone={reload} /> : null}
|
|
183
|
+
<Button variant="outline" size="sm" onClick={() => setTagManager(true)}><Tags className="size-4" aria-hidden />Manage tags</Button>
|
|
147
184
|
<Button variant="outline" size="sm" onClick={() => setSheet("dupes")}><GitMerge className="size-4" aria-hidden />Duplicates{pending ? ` (${pending})` : ""}</Button>
|
|
148
185
|
<Button variant="outline" size="sm" onClick={() => setSheet("import")}><Upload className="size-4" aria-hidden />Import</Button>
|
|
149
186
|
<Button size="sm" onClick={() => setSheet("new")}><Plus className="size-4" aria-hidden />New contact</Button>
|
|
@@ -173,7 +210,17 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
173
210
|
{SENIORITY.map((s) => <option key={s} value={s}>{s}</option>)}
|
|
174
211
|
</select>
|
|
175
212
|
<input aria-label="Filter by country" placeholder="Country" className="h-9 w-32 rounded-md border bg-background px-3 text-sm" value={draft.country} onChange={(e) => set("country", e.target.value)} />
|
|
176
|
-
|
|
213
|
+
<MoreFilters activeCount={MORE_KEYS.filter((k) => draft[k]).length}>
|
|
214
|
+
<TextFilter label="email" type="email" value={draft.email} onChange={(v) => set("email", v)} className="w-44" />
|
|
215
|
+
<TextFilter label="company name" placeholder="Company" value={draft.company_name} onChange={(v) => set("company_name", v)} />
|
|
216
|
+
<TextFilter label="company ID" placeholder="Company ID" type="number" value={draft.company_id} onChange={(v) => set("company_id", v)} className="w-28" />
|
|
217
|
+
<TextFilter label="industry" value={draft.industry} onChange={(v) => set("industry", v)} />
|
|
218
|
+
<TextFilter label="department" placeholder="Department" value={draft.job_department} onChange={(v) => set("job_department", v)} />
|
|
219
|
+
<TextFilter label="state" value={draft.state} onChange={(v) => set("state", v)} className="w-28" />
|
|
220
|
+
<TextFilter label="city" value={draft.city} onChange={(v) => set("city", v)} className="w-32" />
|
|
221
|
+
{listId == null ? <ListSelect label="Filter by list" emptyOption="Any list" value={draft.list} valueLabel={`List ${draft.list}`} pageSize={100} onChange={(l) => set("list", l ? String(l.id) : "")} /> : null}
|
|
222
|
+
</MoreFilters>
|
|
223
|
+
{hasFilters ? <Button variant="ghost" size="sm" onClick={urlFilters.clear}>Clear filters</Button> : null}
|
|
177
224
|
</>
|
|
178
225
|
}
|
|
179
226
|
/>
|
|
@@ -182,6 +229,10 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
182
229
|
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm" role="status">
|
|
183
230
|
<span className="tabular-nums">{selection.ids.length} selected</span>
|
|
184
231
|
{props.renderBulkActions?.({ ids: selection.ids, rows: selection.rows, clear: selection.clear })}
|
|
232
|
+
<TagAssignButton contactIds={selection.ids} onDone={reload} />
|
|
233
|
+
<BulkUpdateButton contactIds={selection.ids} onDone={reload} />
|
|
234
|
+
<VerifyEmailButton rows={selectedRows} onDone={reload} />
|
|
235
|
+
<UnlockInfoButton rows={selectedRows} onDone={reload} />
|
|
185
236
|
<Button variant="destructive" size="sm" onClick={() => setConfirmDelete(true)}><Trash2 className="size-4" aria-hidden />Delete {selection.ids.length}</Button>
|
|
186
237
|
<Button variant="ghost" size="sm" onClick={selection.clear}>Clear selection</Button>
|
|
187
238
|
</div>
|
|
@@ -199,12 +250,13 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
199
250
|
<SortHead label="Company" sortKey="company" sort={sort} onSort={toggleSort} />
|
|
200
251
|
<TableHead>Email</TableHead>
|
|
201
252
|
<SortHead label="Location" sortKey="location" sort={sort} onSort={toggleSort} />
|
|
202
|
-
|
|
253
|
+
<SortHead label="Owner" sortKey="owner" sort={sort} onSort={toggleSort} />
|
|
254
|
+
<TableHead className="w-8"><span className="sr-only">Actions</span></TableHead>
|
|
203
255
|
</TableRow>
|
|
204
256
|
</TableHeader>
|
|
205
257
|
<TableBody>
|
|
206
258
|
{list.loading && rows.length === 0
|
|
207
|
-
? Array.from({ length: 6 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={
|
|
259
|
+
? Array.from({ length: 6 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={8}><Skeleton className="h-6 w-full" /></TableCell></TableRow>)
|
|
208
260
|
: rows.map((row) => {
|
|
209
261
|
const name = contactName(row);
|
|
210
262
|
return (
|
|
@@ -219,7 +271,10 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
219
271
|
<TableCell><Cell value={row.company_name} /></TableCell>
|
|
220
272
|
<TableCell><Cell value={row.work_email} /></TableCell>
|
|
221
273
|
<TableCell><Cell value={location(row)} /></TableCell>
|
|
222
|
-
|
|
274
|
+
<TableCell><Cell value={row.owner_name} /></TableCell>
|
|
275
|
+
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
276
|
+
<span className="flex items-center gap-1">{props.renderRowActions?.(row, { reload })}<ContactRowActions row={row} onDone={reload} /></span>
|
|
277
|
+
</TableCell>
|
|
223
278
|
</TableRow>
|
|
224
279
|
);
|
|
225
280
|
})}
|
|
@@ -229,7 +284,7 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
229
284
|
<EmptyState
|
|
230
285
|
title={hasFilters ? "No contacts match these filters" : "No contacts yet"}
|
|
231
286
|
description={hasFilters ? "Try fewer filters." : "Add your first contact or import a file."}
|
|
232
|
-
action={hasFilters ? <Button variant="outline" onClick={
|
|
287
|
+
action={hasFilters ? <Button variant="outline" onClick={urlFilters.clear}>Clear filters</Button> : <Button onClick={() => setSheet("new")}>New contact</Button>}
|
|
233
288
|
/>
|
|
234
289
|
) : null}
|
|
235
290
|
<Pager page={list.page} pageCount={list.pageCount} total={list.total} noun="contacts" hasNext={list.hasNext} loading={list.loading} onPage={list.setPage} />
|
|
@@ -255,6 +310,7 @@ export function ContactsScreen(props: ContactsScreenProps) {
|
|
|
255
310
|
<DedupeReview entity="contact" onChanged={reload} />
|
|
256
311
|
</FormSheet>
|
|
257
312
|
|
|
313
|
+
<TagManager open={tagManager} onOpenChange={setTagManager} onChanged={reload} />
|
|
258
314
|
<ConfirmSpendDialog
|
|
259
315
|
open={confirmDelete}
|
|
260
316
|
tier="destructive"
|
|
@@ -326,7 +382,7 @@ function ContactPanel({ full, setFull, contactId, seed, onClose, onChanged, onDe
|
|
|
326
382
|
<>
|
|
327
383
|
<DetailPanel
|
|
328
384
|
title={name}
|
|
329
|
-
subtitle={
|
|
385
|
+
subtitle={contactSubtitle(contact)}
|
|
330
386
|
avatar={<EntityAvatar name={name} kind="person" size="lg" />}
|
|
331
387
|
badges={isMasked(contact.work_email) ? <StatusPill tone="warning" dot>Locked email</StatusPill> : undefined}
|
|
332
388
|
onClose={onClose}
|
|
@@ -334,6 +390,7 @@ function ContactPanel({ full, setFull, contactId, seed, onClose, onChanged, onDe
|
|
|
334
390
|
onCollapse={full ? () => setFull(false) : undefined}
|
|
335
391
|
actions={
|
|
336
392
|
<>
|
|
393
|
+
{partial ? null : <><VerifyEmailButton rows={[contact]} onDone={() => { void query.refetch(); onChanged(); }} /><UnlockInfoButton rows={[contact]} onDone={() => { void query.refetch(); onChanged(); }} /></>}
|
|
337
394
|
{partial ? null : renderActions?.(contact, { reload: () => { void query.refetch(); bump(); onChanged(); } })}
|
|
338
395
|
{contact.company?.id && onOpenCompany ? <Button variant="outline" size="sm" onClick={() => onOpenCompany(contact.company!.id as number)}><Building2 className="size-4" aria-hidden />Open company</Button> : null}
|
|
339
396
|
<Button variant="outline" size="sm" disabled={partial} title={partial ? "Loading the full record" : undefined} onClick={() => setEditing(true)}>Edit</Button>
|
|
@@ -341,10 +398,12 @@ function ContactPanel({ full, setFull, contactId, seed, onClose, onChanged, onDe
|
|
|
341
398
|
</>
|
|
342
399
|
}
|
|
343
400
|
tabs={[
|
|
344
|
-
{ id: "overview", label: "Overview", content: overview },
|
|
401
|
+
{ id: "overview", label: "Overview", content: <div className="flex flex-col gap-4"><ContactTagChips contactId={contactId} onChanged={onChanged} />{overview}</div> },
|
|
345
402
|
{ id: "activity", label: "Activity", content: <ActivityTimeline entityType="contact" entityId={contactId} refreshKey={feedKey} /> },
|
|
346
403
|
{ id: "notes", label: "Notes", content: <NotesPanel entity="contact" entityId={contactId} onChanged={bump} /> },
|
|
347
404
|
{ id: "tasks", label: "Tasks", content: <TaskList entity="contact" entityId={contactId} onChanged={bump} /> },
|
|
405
|
+
{ id: "documents", label: "AI documents", content: <ContactDocumentsTab contactId={contactId} contactName={name} /> },
|
|
406
|
+
{ id: "insights", label: "Insights", content: <ContactInsightsTab contactId={contactId} /> },
|
|
348
407
|
...(partial ? [] : extraTabs).map((t) => ({ id: t.id, label: t.label, content: t.render(contact) })),
|
|
349
408
|
]}
|
|
350
409
|
/>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { useState } from "react";
|
|
22
22
|
import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@graph8/sdk/ui";
|
|
23
23
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
24
25
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
26
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
27
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -53,6 +54,9 @@ function side(pair: Pair, s: "a" | "b", entity: "contact" | "company") {
|
|
|
53
54
|
return { id: pair[`${s}_id`] as number, name: name || String([pair[`${s}_email`], pair[`${s}_domain`]].find((v) => v && !isMasked(v)) ?? `#${pair[`${s}_id`]}`), lines: lines.filter(Boolean).map((l) => (isMasked(l) ? "Locked" : String(l))) };
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** The recent-merges API has no paging: a full read is announced under the list. */
|
|
58
|
+
const RECENT_LIMIT = 25;
|
|
59
|
+
|
|
56
60
|
export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps) {
|
|
57
61
|
const brand = useBrand();
|
|
58
62
|
const plural = entity === "contact" ? "contacts" : "companies";
|
|
@@ -67,7 +71,7 @@ export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps
|
|
|
67
71
|
const res = await g8Get<{ items: Pair[]; next_cursor?: string | null }>("/duplicates", { entity_type: entity, status: "pending", limit: 25, cursor }, { signal });
|
|
68
72
|
return { data: res.data.items ?? [], pagination: { next_cursor: res.data.next_cursor ?? null } };
|
|
69
73
|
}, [entity]);
|
|
70
|
-
const recent = useG8Query((signal) => g8Get<MergeRow[]>("/duplicates/recent-merges", { entity_type: entity, limit:
|
|
74
|
+
const recent = useG8Query((signal) => g8Get<MergeRow[]>("/duplicates/recent-merges", { entity_type: entity, limit: RECENT_LIMIT }, { signal }), [entity]);
|
|
71
75
|
const [busy, setBusy] = useState<string | null>(null);
|
|
72
76
|
const dropPick = (id: string | undefined) => id && setPicked((p) => { const n = new Set(p); n.delete(id); return n; });
|
|
73
77
|
const refreshAll = () => { void pairs.reload(); void counts.refetch(); void recent.refetch(); onChanged?.(); };
|
|
@@ -145,6 +149,7 @@ export function DedupeReview({ entity, onChanged, className }: DedupeReviewProps
|
|
|
145
149
|
{m.can_undo ? <Button size="sm" variant="outline" loading={undo.loading && busy === m.id} onClick={() => void undo.mutate(m.id)}>Undo</Button> : null}
|
|
146
150
|
</div>
|
|
147
151
|
))}
|
|
152
|
+
<FirstNotice shown={(recent.data?.data ?? []).length} limit={RECENT_LIMIT} noun="merges" />
|
|
148
153
|
</TabsContent>
|
|
149
154
|
</Tabs>
|
|
150
155
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { useEffect, useState } from "react";
|
|
21
21
|
import { Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
22
22
|
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ListSelect } from "./list-select";
|
|
23
24
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
26
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -38,7 +39,6 @@ export interface ImportWizardProps {
|
|
|
38
39
|
export interface ImportResult { imported?: number | null; updated?: number | null; skipped?: number | null; failed?: number | null; list_id?: number | null }
|
|
39
40
|
interface Upload { datafile_id: string; row_count?: number | null; columns: string[] }
|
|
40
41
|
interface Preview { rows: Array<Record<string, unknown>>; total: number }
|
|
41
|
-
interface ListOption { id: number; title: string; type?: string | null }
|
|
42
42
|
|
|
43
43
|
export const IMPORT_TARGETS: Array<{ value: string; label: string }> = [
|
|
44
44
|
{ value: "CONTACT_FIRST_NAME", label: "First name" }, { value: "CONTACT_LAST_NAME", label: "Last name" },
|
|
@@ -80,7 +80,6 @@ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, c
|
|
|
80
80
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
81
81
|
const [result, setResult] = useState<ImportResult | null>(null);
|
|
82
82
|
|
|
83
|
-
const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }), [], { enabled: upload != null && listId == null });
|
|
84
83
|
const send = useG8Mutation((file: File) => { const body = new FormData(); body.append("file", file); return g8Post<Upload>("/imports", body); }, {
|
|
85
84
|
onSuccess: (r) => { setUpload(r.data); setMapping(guessMapping(r.data.columns ?? [])); },
|
|
86
85
|
});
|
|
@@ -107,7 +106,7 @@ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, c
|
|
|
107
106
|
if (result) {
|
|
108
107
|
return (
|
|
109
108
|
<div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
|
|
110
|
-
<h3 className="text-base font-
|
|
109
|
+
<h3 className="text-base font-medium">Import finished</h3>
|
|
111
110
|
<p className="text-sm">{result.imported ?? 0} new, {result.updated ?? 0} updated, {result.skipped ?? 0} skipped, {result.failed ?? 0} failed.</p>
|
|
112
111
|
<Button variant="outline" onClick={() => { setResult(null); setUpload(null); setMapping({}); setDestination(listId); setNewListTitle(""); setPolicy("autofill"); setEnrich(false); run.reset(); preview.reset(); }}>Import another file</Button>
|
|
113
112
|
</div>
|
|
@@ -161,10 +160,7 @@ export function ImportWizard({ listId, onImported, maxBytes = 5 * 1024 * 1024, c
|
|
|
161
160
|
<h3 className="text-sm font-medium">3. Destination and rules</h3>
|
|
162
161
|
{listId == null ? (
|
|
163
162
|
<div className="flex flex-wrap items-center gap-2">
|
|
164
|
-
<
|
|
165
|
-
<option value="">New list...</option>
|
|
166
|
-
{(lists.data?.data ?? []).filter((l) => !l.type || l.type === "contacts").map((l) => <option key={l.id} value={l.id}>{l.title}</option>)}
|
|
167
|
-
</select>
|
|
163
|
+
<ListSelect label="Destination list" emptyOption="New list..." type="contacts" pageSize={200} value={destination} valueLabel={newListTitle.trim() || undefined} enabled={upload != null && listId == null} selectClassName="h-9 rounded-md border bg-background px-2 text-sm" onChange={(l) => setDestination(l ? l.id : undefined)} />
|
|
168
164
|
{destination == null ? <Input aria-label="New list name" placeholder="New list name" className="w-56" value={newListTitle} onChange={(e) => setNewListTitle(e.target.value)} /> : null}
|
|
169
165
|
</div>
|
|
170
166
|
) : null}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ListSelect: the audience-list dropdown with the list read wired in. A native select that is searchable (server
|
|
5
|
+
* search once there is more than one page) and paged ("Load more lists"), so the 201st list is reachable. It is the
|
|
6
|
+
* foundation's ListSelect plus `GET /lists` (the foundation holds no API scope); every block that picks a list
|
|
7
|
+
* (forms, lists, newsletter, nurture, sequencer, dialer) imports this one.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. GET /lists?search&page&limit (lists.listLists) read - through lib/read-lists.ts
|
|
11
|
+
*/
|
|
12
|
+
import { ListSelect as BaseListSelect } from "@/g8/components/list-select";
|
|
13
|
+
import type { ListSelectProps } from "@/g8/components/list-select";
|
|
14
|
+
import { readLists } from "../lib/read-lists";
|
|
15
|
+
|
|
16
|
+
export type { ListSelectOption } from "@/g8/components/list-select";
|
|
17
|
+
|
|
18
|
+
export function ListSelect(props: Omit<ListSelectProps, "read">) {
|
|
19
|
+
return <BaseListSelect {...props} read={readLists} />;
|
|
20
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OwnerAssignListButton: "Assign owner to all N contacts in this list". The ONLY owner action, and deliberately not a
|
|
5
|
+
* selection action.
|
|
6
|
+
*
|
|
7
|
+
* Why not for a selection: `POST /contacts/assign-owner` takes a FILTER (`list_id` + `filterModel`), not ids, and resolves
|
|
8
|
+
* it server-side across EVERY matching contact; its docstring warns the count is unbounded. Addressing a selection through
|
|
9
|
+
* an id filter would reassign the whole list or org if the key were ever ignored or renamed, so selections, rows and the
|
|
10
|
+
* open contact do not get an owner action here. There is no id-addressed owner write in the contract (bulk-update and
|
|
11
|
+
* PATCH take no owner), so that is an API gap: assign-owner needs an `ids` field.
|
|
12
|
+
*
|
|
13
|
+
* SDK operations, in order:
|
|
14
|
+
* 1. listTeamMembers GET /team-members via the injected picker (read, fetched when the picker opens)
|
|
15
|
+
* 2. assignContactOwner POST /contacts/assign-owner { owner_id, list_id, filterModel: {} } (write)
|
|
16
|
+
* Scope = the whole list, nothing else. N comes from the list's own count (the screen's total with no other filter
|
|
17
|
+
* applied), is shown in the confirm, and the button is disabled until that count has loaded. `owner_id: null`
|
|
18
|
+
* clears every owner in the list. Both paths go through ConfirmSpendDialog and name the list and N.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import type { ReactNode } from "react";
|
|
23
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
|
|
24
|
+
import { UserCog } from "lucide-react";
|
|
25
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
28
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { plural } from "@/g8/lib/plural";
|
|
30
|
+
|
|
31
|
+
/** A team member, as the injected picker reports it: the user id the API takes, and a name for the confirm. */
|
|
32
|
+
export interface OwnerChoice { id: string; name: string }
|
|
33
|
+
|
|
34
|
+
export interface OwnerPickerProps {
|
|
35
|
+
value: string | null;
|
|
36
|
+
onChange: (member: OwnerChoice) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface OwnerAssignListButtonProps {
|
|
40
|
+
/**
|
|
41
|
+
* Renders the team-member picker (the pickers block's MemberPicker: `renderPicker={(p) => <MemberPicker label="New owner" {...p} />}`).
|
|
42
|
+
* It is injected so crm does not depend on pickers. Without it the button renders nothing: an owner is never typed as a raw id.
|
|
43
|
+
*/
|
|
44
|
+
renderPicker?: (props: OwnerPickerProps) => ReactNode;
|
|
45
|
+
listId: number;
|
|
46
|
+
/** Shown in the confirm: "Q4 target accounts". Falls back to "list <id>". */
|
|
47
|
+
listName?: string | null;
|
|
48
|
+
/** The number of contacts in the list, from a real read. Null while it loads: the button stays disabled. */
|
|
49
|
+
total: number | null;
|
|
50
|
+
onDone?: (result: { updated: number | null; ownerId: string | null }) => void;
|
|
51
|
+
variant?: "default" | "outline" | "secondary" | "ghost";
|
|
52
|
+
size?: "default" | "sm";
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `updated` from the answer: `{ updated }`, `{ data: { updated } }`; null when the API did not say. */
|
|
57
|
+
export function updatedCount(data: unknown): number | null {
|
|
58
|
+
const o = data as { updated?: unknown; data?: { updated?: unknown } } | null | undefined;
|
|
59
|
+
const n = o?.updated ?? o?.data?.updated;
|
|
60
|
+
return typeof n === "number" ? n : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function OwnerAssignListButton({ renderPicker, listId, listName, total, onDone, variant = "outline", size = "sm", className }: OwnerAssignListButtonProps) {
|
|
64
|
+
const [open, setOpen] = useState(false);
|
|
65
|
+
const [member, setMember] = useState<OwnerChoice | null>(null);
|
|
66
|
+
const [pending, setPending] = useState<"assign" | "remove" | null>(null);
|
|
67
|
+
const [done, setDone] = useState<{ updated: number | null; total: number; removed: boolean } | null>(null);
|
|
68
|
+
const label = listName?.trim() || `list ${listId}`;
|
|
69
|
+
const n = total ?? 0;
|
|
70
|
+
|
|
71
|
+
const run = useG8Mutation(async (v: { ownerId: string | null; total: number }) => {
|
|
72
|
+
const res = await g8Post<unknown>("/contacts/assign-owner", { owner_id: v.ownerId, list_id: listId, filterModel: {} });
|
|
73
|
+
return { updated: updatedCount(res.data), ownerId: v.ownerId, total: v.total };
|
|
74
|
+
}, {
|
|
75
|
+
invalidates: [["contacts", "list"], ["contacts", "detail"], ["contacts", "record"]],
|
|
76
|
+
onSuccess: (r) => { setPending(null); setDone({ updated: r.updated, total: r.total, removed: r.ownerId == null }); onDone?.({ updated: r.updated, ownerId: r.ownerId }); },
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const close = () => { setOpen(false); setMember(null); setDone(null); setPending(null); run.reset(); };
|
|
80
|
+
const short = done && done.updated != null && done.updated !== done.total;
|
|
81
|
+
const ready = total != null && total > 0;
|
|
82
|
+
if (!renderPicker) return null;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<Button className={className} variant={variant} size={size} disabled={!ready} title={total == null ? "Loading the list size" : total === 0 ? "This list has no contacts" : undefined} onClick={() => setOpen(true)}>
|
|
87
|
+
<UserCog className="size-4" aria-hidden />Assign owner to all{total != null ? ` ${total.toLocaleString()}` : ""}
|
|
88
|
+
</Button>
|
|
89
|
+
<Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
|
90
|
+
<DialogContent>
|
|
91
|
+
<DialogHeader>
|
|
92
|
+
<DialogTitle>Assign an owner to the whole list</DialogTitle>
|
|
93
|
+
<DialogDescription>This changes every contact in {label}, {plural(n, "contact")}. It does not use your selection.</DialogDescription>
|
|
94
|
+
</DialogHeader>
|
|
95
|
+
{done ? (
|
|
96
|
+
<div role="status" className="text-sm">
|
|
97
|
+
<p className="font-medium">{done.updated == null ? "Done." : `${done.removed ? "Removed the owner from" : "Assigned the owner to"} ${plural(done.updated, "contact")}.`}</p>
|
|
98
|
+
{short ? <p className="mt-1 text-destructive">The list had {done.total.toLocaleString()} contacts, but the server updated {done.updated!.toLocaleString()}.</p> : null}
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<>
|
|
102
|
+
{renderPicker?.({ value: member?.id ?? null, onChange: (m) => { setMember({ id: m.id, name: m.name }); run.reset(); } })}
|
|
103
|
+
{run.error && !pending ? <ApiError error={run.error} title="Could not change the owner" /> : null}
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
<DialogFooter>
|
|
107
|
+
{done ? <Button onClick={close}>Done</Button> : (
|
|
108
|
+
<>
|
|
109
|
+
<Button variant="ghost" onClick={close}>Cancel</Button>
|
|
110
|
+
<Button variant="outline" disabled={run.loading} onClick={() => setPending("remove")}>Remove all owners</Button>
|
|
111
|
+
<Button disabled={!member || run.loading} onClick={() => setPending("assign")}>Assign owner</Button>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
</DialogFooter>
|
|
115
|
+
</DialogContent>
|
|
116
|
+
</Dialog>
|
|
117
|
+
<ConfirmSpendDialog
|
|
118
|
+
open={pending != null}
|
|
119
|
+
tier="destructive"
|
|
120
|
+
title={pending === "remove" ? `Remove the owner from all ${plural(n, "contact")} in ${label}` : `Make ${member?.name ?? "this person"} the owner of all ${plural(n, "contact")} in ${label}`}
|
|
121
|
+
description={pending === "remove"
|
|
122
|
+
? "Every contact in the list will have no owner. The previous owners are not remembered, so this cannot be undone from here."
|
|
123
|
+
: "Every contact in the list gets this owner, replacing whoever owns it now. The previous owners are not remembered."}
|
|
124
|
+
affected={n}
|
|
125
|
+
confirmLabel={pending === "remove" ? "Remove owners" : "Assign owner"}
|
|
126
|
+
confirming={run.loading}
|
|
127
|
+
onConfirm={() => void run.mutate({ ownerId: pending === "remove" ? null : member?.id ?? null, total: n })}
|
|
128
|
+
onCancel={() => setPending(null)}
|
|
129
|
+
/>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordEngagementTab: how much a contact or company has engaged: emails, calls, meetings, LinkedIn, website visits and
|
|
5
|
+
* downloads, the last touch, and the pages visited in the last 90 days. Use it as an `extraDetailTabs` entry of
|
|
6
|
+
* ContactsScreen / CompaniesScreen:
|
|
7
|
+
* { id: "engagement", label: "Engagement", render: (c) => <RecordEngagementTab kind="contacts" id={c.id} /> }
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. getContactEngagementSummary GET /contacts/{id}/engagement-summary (read) when kind="contacts"
|
|
11
|
+
* getCompanyEngagementSummary GET /companies/{id}/engagement-summary (read) when kind="companies"
|
|
12
|
+
* 2. getContactSitesVisited GET /contacts/{id}/sites-visited?days=90 (read) when kind="contacts"
|
|
13
|
+
* getCompanySitesVisited GET /companies/{id}/sites-visited?days=90 (read) when kind="companies"
|
|
14
|
+
* Nothing here is billable, external or destructive. The visits list is shown ten at a time with "Show more", so the
|
|
15
|
+
* count in the heading always matches what can be reached.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { Activity } from "lucide-react";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
26
|
+
|
|
27
|
+
/** The counts both summaries share (the contact one adds LinkedIn and the last touch). */
|
|
28
|
+
interface Summary { emails?: number | null; calls?: number | null; meetings?: number | null; linkedin?: number | null; website_visits?: number | null; downloads?: number | null; contact_count?: number | null; last_contact?: string | null; last_contact_checked?: boolean | null }
|
|
29
|
+
interface Visit { url?: string | null; domain?: string | null; title?: string | null; event_date?: string | null; page_type?: string | null }
|
|
30
|
+
interface Visits { visits?: Visit[] | null; total_visits?: number | null }
|
|
31
|
+
|
|
32
|
+
const STEP = 10;
|
|
33
|
+
const DAYS = 90;
|
|
34
|
+
|
|
35
|
+
function dataOf<T>(env: unknown): T {
|
|
36
|
+
const d = (env as { data?: unknown } | null)?.data;
|
|
37
|
+
return (d && typeof d === "object" ? d : {}) as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function when(iso: string | null | undefined): string {
|
|
41
|
+
const d = iso ? new Date(iso) : null;
|
|
42
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const count = (n: number | null | undefined): string => (typeof n === "number" ? n.toLocaleString() : "-");
|
|
46
|
+
|
|
47
|
+
export interface RecordEngagementTabProps {
|
|
48
|
+
kind: "contacts" | "companies";
|
|
49
|
+
id: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function RecordEngagementTab({ kind, id }: RecordEngagementTabProps) {
|
|
53
|
+
const [shown, setShown] = useState(STEP);
|
|
54
|
+
const summary = useG8Query(async (signal) => dataOf<Summary>(await g8Get(`/${kind}/${id}/engagement-summary`, undefined, { signal })), [kind, id]);
|
|
55
|
+
const visits = useG8Query(async (signal) => dataOf<Visits>(await g8Get(`/${kind}/${id}/sites-visited`, { days: DAYS }, { signal })), [kind, id]);
|
|
56
|
+
|
|
57
|
+
if (summary.error) return <ApiError error={summary.error} title="Could not load engagement" onRetry={() => void summary.refetch()} />;
|
|
58
|
+
if (!summary.data) return <Skeleton className="h-32 w-full" aria-label="Loading engagement" />;
|
|
59
|
+
const s = summary.data;
|
|
60
|
+
const items = [
|
|
61
|
+
{ label: "Emails", value: count(s.emails) },
|
|
62
|
+
{ label: "Calls", value: count(s.calls) },
|
|
63
|
+
{ label: "Meetings", value: count(s.meetings) },
|
|
64
|
+
...(kind === "contacts" ? [{ label: "LinkedIn", value: count(s.linkedin) }] : [{ label: "People", value: count(s.contact_count) }]),
|
|
65
|
+
{ label: "Website visits", value: count(s.website_visits) },
|
|
66
|
+
{ label: "Downloads", value: count(s.downloads) },
|
|
67
|
+
...(kind === "contacts" ? [{ label: "Last touch", value: when(s.last_contact) || (s.last_contact_checked === false ? "Not checked" : "No touch yet") }] : []),
|
|
68
|
+
];
|
|
69
|
+
const rows = visits.data?.visits ?? [];
|
|
70
|
+
const total = rows.length;
|
|
71
|
+
const reported = visits.data?.total_visits ?? total;
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-4">
|
|
74
|
+
<KeyValueList columns={2} items={items} />
|
|
75
|
+
<section aria-label="Pages visited" className="space-y-2">
|
|
76
|
+
<h3 className="text-sm font-medium">Pages visited, last {DAYS} days</h3>
|
|
77
|
+
{visits.error ? <ApiError error={visits.error} title="Could not load pages visited" onRetry={() => void visits.refetch()} /> : null}
|
|
78
|
+
{!visits.error && !visits.data ? <Skeleton className="h-16 w-full" aria-label="Loading pages visited" /> : null}
|
|
79
|
+
{visits.data && total === 0 ? <EmptyState compact icon={<Activity className="size-8" />} title="No pages visited" description={`No website visits in the last ${DAYS} days.`} /> : null}
|
|
80
|
+
{total > 0 ? (
|
|
81
|
+
<>
|
|
82
|
+
<ul className="divide-y rounded-md border">
|
|
83
|
+
{rows.slice(0, shown).map((v, i) => (
|
|
84
|
+
<li key={`${v.url}-${i}`} className="flex items-center justify-between gap-3 p-3 text-sm">
|
|
85
|
+
<span className="min-w-0">
|
|
86
|
+
<span className="block truncate font-medium">{v.title || v.url || "Page"}</span>
|
|
87
|
+
<span className="block truncate text-xs text-muted-foreground">{[v.domain, v.page_type].filter(Boolean).join(" · ")}</span>
|
|
88
|
+
</span>
|
|
89
|
+
<span className="shrink-0 text-xs text-muted-foreground">{when(v.event_date)}</span>
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ul>
|
|
93
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
94
|
+
<span role="status">Showing {Math.min(shown, total).toLocaleString()} of {total.toLocaleString()} visits</span>
|
|
95
|
+
{shown < total ? <Button size="sm" variant="outline" onClick={() => setShown((n) => n + STEP)}>Show more</Button> : null}
|
|
96
|
+
</div>
|
|
97
|
+
{reported > total ? <p role="note" className="text-xs text-muted-foreground">The API returned {total.toLocaleString()} of {reported.toLocaleString()} visits in this window. Narrow it in the visitors screen to see the rest.</p> : null}
|
|
98
|
+
</>
|
|
99
|
+
) : null}
|
|
100
|
+
</section>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -178,7 +178,7 @@ export function RecordForm(props: RecordFormProps) {
|
|
|
178
178
|
|
|
179
179
|
{validation ? <p role="alert" className="text-sm text-destructive">{validation}</p> : null}
|
|
180
180
|
{duplicate ? (
|
|
181
|
-
<div role="alert" className="rounded-md border border-
|
|
181
|
+
<div role="alert" className="rounded-md border border-warning-foreground/30 bg-warning p-3 text-sm">
|
|
182
182
|
A contact with this email or LinkedIn URL already exists{existingId ? ` (id ${existingId})` : ""}.
|
|
183
183
|
{props.onDuplicate ? <Button type="button" variant="link" size="sm" onClick={() => props.onDuplicate?.(existingId)}>Open the existing contact</Button> : null}
|
|
184
184
|
</div>
|