@graph8/sdk 0.370.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +6 -0
- package/blocks/crm/block.json +53 -4
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +2 -2
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +4 -1
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +51 -17
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +113 -1
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +36 -8
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +8 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +20 -5
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -98,7 +98,7 @@ export function BookingPage({ eventTypeId, daysAhead = 14, timeZone, defaultAtte
|
|
|
98
98
|
if (book.data) {
|
|
99
99
|
return (
|
|
100
100
|
<div className={`space-y-2 rounded-lg border p-4 ${className ?? ""}`} role="status">
|
|
101
|
-
<h2 className="text-base font-
|
|
101
|
+
<h2 className="text-base font-medium">You are booked</h2>
|
|
102
102
|
<p className="text-sm">
|
|
103
103
|
{eventType.data?.title ?? "Meeting"} on {formatDateTime(bookedAt ?? slot?.time, zone)}. A calendar invite was sent to {email}.
|
|
104
104
|
</p>
|
|
@@ -116,7 +116,7 @@ export function BookingPage({ eventTypeId, daysAhead = 14, timeZone, defaultAtte
|
|
|
116
116
|
return (
|
|
117
117
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
118
118
|
<header>
|
|
119
|
-
<h2 className="text-base font-
|
|
119
|
+
<h2 className="text-base font-medium">{eventType.data?.title ?? "Book a meeting"}</h2>
|
|
120
120
|
<p className="text-sm text-muted-foreground">
|
|
121
121
|
{eventType.data ? `${eventType.data.length} minutes · ` : ""}times shown in {zone}
|
|
122
122
|
</p>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MeetingContactLinks: "Open <attendee>" buttons for a booked or recorded meeting, one per attendee (at most three)
|
|
5
|
+
* who is a contact in your CRM. Use it in MeetingsScreen's `renderDetailActions`:
|
|
6
|
+
* renderDetailActions={(selection) => <MeetingContactLinks selection={selection} onOpenContact={(id) => router.push(`/contacts?id=${id}`)} />}
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getBooking GET /appointments/bookings/{uid} (read) for a booking
|
|
10
|
+
* getMeeting GET /inbox/meetings/{id} (read) for a recorded meeting
|
|
11
|
+
* 2. listContacts GET /contacts?email= (read) once per attendee, via the crm ContactLookupLink
|
|
12
|
+
* Nothing here is billable, external or destructive.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
16
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
|
+
import { ContactLookupLink } from "@/g8/crm/components/contact-lookup-link";
|
|
18
|
+
import { unwrap } from "../lib/appointments-types";
|
|
19
|
+
import type { MeetingSelection } from "./meeting-list";
|
|
20
|
+
|
|
21
|
+
interface Attendee { email?: string | null; name?: string | null }
|
|
22
|
+
|
|
23
|
+
export interface MeetingContactLinksProps {
|
|
24
|
+
selection: MeetingSelection;
|
|
25
|
+
/** Route to the contact in your contacts screen. */
|
|
26
|
+
onOpenContact: (contactId: number) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function MeetingContactLinks({ selection, onOpenContact }: MeetingContactLinksProps) {
|
|
30
|
+
const { kind, id } = selection;
|
|
31
|
+
const q = useG8Query(async (signal) => {
|
|
32
|
+
const path = kind === "booking" ? `/appointments/bookings/${encodeURIComponent(id)}` : `/inbox/meetings/${encodeURIComponent(id)}`;
|
|
33
|
+
return unwrap<{ attendees?: Attendee[] | null }>(await g8Get(path, undefined, { signal })).attendees ?? [];
|
|
34
|
+
}, [kind, id]);
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
const people = (q.data ?? []).filter((a) => {
|
|
37
|
+
const email = a.email?.trim().toLowerCase();
|
|
38
|
+
if (!email || seen.has(email)) return false;
|
|
39
|
+
seen.add(email);
|
|
40
|
+
return true;
|
|
41
|
+
}).slice(0, 3);
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{people.map((a) => (
|
|
45
|
+
<ContactLookupLink key={a.email} email={a.email} label={`Open ${a.name || a.email}`} onOpen={onOpenContact} />
|
|
46
|
+
))}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -48,7 +48,7 @@ function RecordedDetail({ id, className }: { id: string; className?: string }) {
|
|
|
48
48
|
const m = q.data;
|
|
49
49
|
return (
|
|
50
50
|
<section className={`space-y-3 ${className ?? ""}`}>
|
|
51
|
-
<h2 className="text-base font-
|
|
51
|
+
<h2 className="text-base font-medium">{m.subject || "Untitled meeting"}</h2>
|
|
52
52
|
<dl className="grid gap-3 sm:grid-cols-2">
|
|
53
53
|
<Field label="When">{formatDateTime(m.start_time)}</Field>
|
|
54
54
|
<Field label="Attendees">{(m.attendees ?? []).map((a) => a.name || a.email).join(", ") || "Unknown"}</Field>
|
|
@@ -94,7 +94,7 @@ function BookingDetail({ uid, onChanged, className }: { uid: string; onChanged?:
|
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<section className={`space-y-4 ${className ?? ""}`}>
|
|
97
|
-
<h2 className="text-base font-
|
|
97
|
+
<h2 className="text-base font-medium">{b.title || b.event_type_title || "Meeting"}</h2>
|
|
98
98
|
<dl className="grid gap-3 sm:grid-cols-2">
|
|
99
99
|
<Field label="When">{formatDateTime(b.start_time)}</Field>
|
|
100
100
|
<Field label="Status"><span className="capitalize">{b.status}</span></Field>
|
|
@@ -18,6 +18,8 @@ import { ApiError } from "@/g8/components/api-error";
|
|
|
18
18
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
19
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
20
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
22
|
+
import { SelectFilter, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
21
23
|
import { formatDateTime, listOf, type Booking, type RecordedMeeting } from "../lib/appointments-types";
|
|
22
24
|
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
23
25
|
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
@@ -45,8 +47,8 @@ const VIEWS: ReadonlyArray<{ value: MeetingView; label: string }> = [
|
|
|
45
47
|
{ value: "recorded", label: "Recorded" },
|
|
46
48
|
];
|
|
47
49
|
|
|
48
|
-
function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number) {
|
|
49
|
-
const base = { skip: page * take, take };
|
|
50
|
+
function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number, attendeeEmail?: string) {
|
|
51
|
+
const base = { skip: page * take, take, attendee_email: attendeeEmail || undefined };
|
|
50
52
|
if (view === "upcoming") return { ...base, after_start: now, sort_by: "start_time", sort_order: "asc" };
|
|
51
53
|
if (view === "past") return { ...base, before_end: now, sort_by: "start_time", sort_order: "desc" };
|
|
52
54
|
return { ...base, status: "cancelled", sort_by: "start_time", sort_order: "desc" };
|
|
@@ -56,36 +58,38 @@ function attendeeNames(b: Booking): string {
|
|
|
56
58
|
return b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees";
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
useEffect(() => { const t = setTimeout(() => setV(value.trim()), ms); return () => clearTimeout(t); }, [value, ms]);
|
|
62
|
-
return v;
|
|
63
|
-
}
|
|
61
|
+
/** Every filter the meetings (/inbox/meetings) and bookings APIs take besides the view tab. */
|
|
62
|
+
const MEETING_FILTERS = ["search", "participant_email", "timeframe", "has_transcript", "attendee_email"] as const;
|
|
64
63
|
|
|
65
64
|
export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
|
|
66
65
|
const [view, setView] = useState<MeetingView>(initialView);
|
|
67
66
|
const [page, setPage] = useState(0);
|
|
68
|
-
|
|
69
|
-
const
|
|
67
|
+
// Every filter the meetings and bookings APIs take lives in the URL (`m_participant_email=...`).
|
|
68
|
+
const uf = useUrlFilters(MEETING_FILTERS, { prefix: "m_" });
|
|
69
|
+
const search = uf.draft.search;
|
|
70
|
+
const query = uf.filters.search.trim();
|
|
71
|
+
const { participant_email, timeframe, has_transcript, attendee_email } = uf.filters;
|
|
70
72
|
const now = useMemo(() => new Date().toISOString(), [view]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
71
73
|
|
|
72
74
|
const rows = useG8Query(
|
|
73
75
|
async (signal) => {
|
|
74
76
|
if (view === "recorded") {
|
|
75
|
-
const env = await g8Get("/inbox/meetings", { search: query || undefined, page: page + 1, page_size: pageSize }, { signal });
|
|
77
|
+
const env = await g8Get("/inbox/meetings", { search: query || undefined, participant_email: participant_email ? participant_email.split(",").map((e) => e.trim()).filter(Boolean) : undefined, timeframe: timeframe || undefined, has_transcript: has_transcript ? has_transcript === "yes" : undefined, page: page + 1, page_size: pageSize }, { signal });
|
|
76
78
|
return { kind: "meeting" as const, items: listOf<RecordedMeeting>(env), more: Boolean((env as { pagination?: { has_next?: boolean } }).pagination?.has_next) };
|
|
77
79
|
}
|
|
78
|
-
const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize), { signal });
|
|
80
|
+
const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize, attendee_email.trim()), { signal });
|
|
79
81
|
const items = listOf<Booking>(env, "bookings");
|
|
80
82
|
return { kind: "booking" as const, items, more: items.length === pageSize };
|
|
81
83
|
},
|
|
82
|
-
[view, page, query, pageSize, now, refreshKey],
|
|
84
|
+
[view, page, query, participant_email, timeframe, has_transcript, attendee_email, pageSize, now, refreshKey],
|
|
85
|
+
{ enabled: uf.ready },
|
|
83
86
|
);
|
|
84
87
|
|
|
85
88
|
const switchView = (next: MeetingView) => {
|
|
86
89
|
setView(next);
|
|
87
90
|
setPage(0);
|
|
88
91
|
};
|
|
92
|
+
useEffect(() => { setPage(0); }, [query, participant_email, timeframe, has_transcript, attendee_email]);
|
|
89
93
|
const result = rows.data;
|
|
90
94
|
|
|
91
95
|
return (
|
|
@@ -98,7 +102,19 @@ export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pag
|
|
|
98
102
|
</Button>
|
|
99
103
|
))}
|
|
100
104
|
</div>
|
|
101
|
-
{view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) =>
|
|
105
|
+
{view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) => uf.set("search", e.target.value)} /> : null}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
108
|
+
{view === "recorded" ? (
|
|
109
|
+
<>
|
|
110
|
+
<TextFilter label="participant email" placeholder="Participant email" type="email" value={uf.draft.participant_email} onChange={(v) => uf.set("participant_email", v)} className="w-52" />
|
|
111
|
+
<SelectFilter label="timeframe" any="Past (default)" value={uf.draft.timeframe} onChange={(v) => uf.set("timeframe", v)} options={[{ value: "upcoming", label: "Upcoming" }, { value: "all", label: "All" }]} />
|
|
112
|
+
<SelectFilter label="transcript" any="With or without transcript" value={uf.draft.has_transcript} onChange={(v) => uf.set("has_transcript", v)} options={[{ value: "yes", label: "Has a transcript" }, { value: "no", label: "No transcript" }]} />
|
|
113
|
+
</>
|
|
114
|
+
) : (
|
|
115
|
+
<TextFilter label="attendee email" placeholder="Attendee email" type="email" value={uf.draft.attendee_email} onChange={(v) => uf.set("attendee_email", v)} className="w-52" />
|
|
116
|
+
)}
|
|
117
|
+
{uf.active ? <Button size="sm" variant="ghost" onClick={uf.clear}>Clear filters</Button> : null}
|
|
102
118
|
</div>
|
|
103
119
|
|
|
104
120
|
{rows.error ? (
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordMeetingsTab: the meetings booked with one contact, past and upcoming, newest first, each opening in the meetings
|
|
5
|
+
* screen. Use it as an `extraDetailTabs` entry of ContactsScreen:
|
|
6
|
+
* { id: "meetings", label: "Meetings", render: (c) => <RecordMeetingsTab contactId={c.id} onOpenMeeting={(sel) => router.push(`/meetings?sel=${sel}`)} /> }
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getContactMeetings GET /contacts/{id}/meetings (read; booking events, the API answers the 50 most recent)
|
|
10
|
+
* Nothing here is billable, external or destructive. Book a new meeting from the scheduling screen.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
14
|
+
import { CalendarDays } from "lucide-react";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
17
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
18
|
+
import type { Tone } from "@/g8/components/screen/status-pill";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
21
|
+
import { listOf } from "../lib/appointments-types";
|
|
22
|
+
|
|
23
|
+
/** One row of GET /contacts/{id}/meetings: a booking event. */
|
|
24
|
+
interface BookingEvent { event?: string | null; timestamp?: string | null; booking_id?: string | null; title?: string | null; start_time?: string | null; end_time?: string | null; attendees?: unknown }
|
|
25
|
+
|
|
26
|
+
/** The server returns at most this many events, newest first. */
|
|
27
|
+
export const MEETINGS_TAB_API_LIMIT = 50;
|
|
28
|
+
|
|
29
|
+
const EVENT: Record<string, { label: string; tone: Tone }> = {
|
|
30
|
+
"appointments.booking_created": { label: "Booked", tone: "info" },
|
|
31
|
+
"appointments.booking_rescheduled": { label: "Rescheduled", tone: "warning" },
|
|
32
|
+
"appointments.booking_cancelled": { label: "Cancelled", tone: "negative" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function when(iso: string | null | undefined): string {
|
|
36
|
+
const d = iso ? new Date(iso) : null;
|
|
37
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function attendeeNames(a: unknown): string {
|
|
41
|
+
if (!Array.isArray(a)) return "";
|
|
42
|
+
return a.map((x) => (typeof x === "string" ? x : ((x as { name?: string; email?: string } | null)?.name || (x as { email?: string } | null)?.email || ""))).filter(Boolean).join(", ");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RecordMeetingsTabProps {
|
|
46
|
+
contactId: number;
|
|
47
|
+
/** Route to the booking in your meetings screen; receives the `sel` value ("booking:<id>"). Without it the rows link to `${meetingsPath}?sel=booking:<id>`. */
|
|
48
|
+
onOpenMeeting?: (selection: string) => void;
|
|
49
|
+
/** Path of your meetings screen for the plain links. Default "/meetings". */
|
|
50
|
+
meetingsPath?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function RecordMeetingsTab({ contactId, onOpenMeeting, meetingsPath = "/meetings" }: RecordMeetingsTabProps) {
|
|
54
|
+
const q = useG8Query(async (signal) => listOf<BookingEvent>(await g8Get(`/contacts/${contactId}/meetings`, undefined, { signal }), "items"), [contactId]);
|
|
55
|
+
if (q.error) return <ApiError error={q.error} title="Could not load meetings" onRetry={() => void q.refetch()} />;
|
|
56
|
+
if (!q.data) return <Skeleton className="h-24 w-full" aria-label="Loading meetings" />;
|
|
57
|
+
if (q.data.length === 0) return <EmptyState compact icon={<CalendarDays className="size-8" />} title="No meetings yet" description="Meetings booked with this contact show up here." />;
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
<ul className="divide-y rounded-md border" aria-label="Meetings">
|
|
61
|
+
{q.data.map((m, i) => {
|
|
62
|
+
const kind = EVENT[m.event ?? ""] ?? { label: "Booking", tone: "neutral" as Tone };
|
|
63
|
+
const selection = m.booking_id ? `booking:${m.booking_id}` : null;
|
|
64
|
+
const title = m.title || "Meeting";
|
|
65
|
+
const detail = [when(m.start_time ?? m.timestamp), attendeeNames(m.attendees)].filter(Boolean).join(" · ");
|
|
66
|
+
return (
|
|
67
|
+
<li key={`${m.booking_id ?? "event"}-${m.event}-${i}`} className="flex items-center justify-between gap-3 p-3 text-sm">
|
|
68
|
+
<div className="min-w-0">
|
|
69
|
+
{selection ? (
|
|
70
|
+
<a
|
|
71
|
+
href={`${meetingsPath}?sel=${encodeURIComponent(selection)}`}
|
|
72
|
+
className="block truncate font-medium hover:underline"
|
|
73
|
+
onClick={(e) => {
|
|
74
|
+
if (!onOpenMeeting || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
onOpenMeeting(selection);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{title}
|
|
80
|
+
</a>
|
|
81
|
+
) : <span className="font-medium">{title}</span>}
|
|
82
|
+
{detail ? <p className="truncate text-xs text-muted-foreground">{detail}</p> : null}
|
|
83
|
+
</div>
|
|
84
|
+
<StatusPill tone={kind.tone} dot>{kind.label}</StatusPill>
|
|
85
|
+
</li>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</ul>
|
|
89
|
+
{q.data.length >= MEETINGS_TAB_API_LIMIT ? <p role="note" className="text-xs text-muted-foreground">Showing the {MEETINGS_TAB_API_LIMIT} most recent booking events. Older ones are in the meetings screen.</p> : null}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -86,7 +86,7 @@ function Editor({ campaignId, doc, onChanged, onClosed }: { campaignId: string;
|
|
|
86
86
|
{full.data ? (
|
|
87
87
|
<>
|
|
88
88
|
<Textarea aria-label="Document content" className="min-h-48 font-mono text-sm" value={body} onChange={(e) => setContent(e.target.value)} />
|
|
89
|
-
{tooBig ? <p role="alert" className="text-sm text-
|
|
89
|
+
{tooBig ? <p role="alert" className="text-sm text-error-foreground">This document is over 5 MiB and cannot be saved through the proxy.</p> : null}
|
|
90
90
|
<div className="flex gap-2">
|
|
91
91
|
<Button disabled={content === null || tooBig || poll.polling} onClick={() => setPending({ kind: "save" })}>Save changes</Button>
|
|
92
92
|
<Button variant="outline" disabled={poll.polling} onClick={() => setPending({ kind: "regenerate" })}>Rewrite with AI</Button>
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { useEffect, useState } from "react";
|
|
35
35
|
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
36
36
|
import { ApiError } from "@/g8/components/api-error";
|
|
37
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
37
38
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
38
39
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
39
40
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -55,7 +56,7 @@ interface Mailbox { id: string | number; email?: string | null; connection_statu
|
|
|
55
56
|
|
|
56
57
|
function Check({ ok, children, detail }: { ok: boolean | undefined; children: string; detail?: string }) {
|
|
57
58
|
return (
|
|
58
|
-
<li className={ok ? "text-
|
|
59
|
+
<li className={ok ? "text-positive-foreground" : "text-warning-foreground"}>
|
|
59
60
|
<span aria-hidden>{ok ? "Ready: " : "Missing: "}</span>{children}
|
|
60
61
|
{!ok && detail ? <span className="block pl-4 text-xs text-muted-foreground">{detail}</span> : null}
|
|
61
62
|
</li>
|
|
@@ -64,11 +65,14 @@ function Check({ ok, children, detail }: { ok: boolean | undefined; children: st
|
|
|
64
65
|
|
|
65
66
|
const day = (d: Date) => d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
66
67
|
|
|
68
|
+
/** The mailboxes API has no paging: a full read is announced under the sender list. */
|
|
69
|
+
const MAILBOX_LIMIT = 200;
|
|
70
|
+
|
|
67
71
|
export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, className }: CampaignLaunchStepProps) {
|
|
68
72
|
const brand = useBrand();
|
|
69
73
|
const campaign = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/full`, undefined, { signal }), [campaignId]);
|
|
70
74
|
const generation = useG8Query((signal) => g8Get(`/campaigns/${campaignId}/status`, undefined, { signal }), [campaignId]);
|
|
71
|
-
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit:
|
|
75
|
+
const mailboxes = useG8Query((signal) => g8Get("/mailboxes", { limit: MAILBOX_LIMIT }, { signal }), []);
|
|
72
76
|
const verdict = useG8Query((signal) => g8Get("/deliverability/safe-to-send", undefined, { signal }), []);
|
|
73
77
|
const [chosen, setChosen] = useState<Set<string> | null>(null);
|
|
74
78
|
const [confirm, setConfirm] = useState(false);
|
|
@@ -136,20 +140,20 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
136
140
|
if (result) {
|
|
137
141
|
const channels = (result.channels_attached as Array<{ email?: string }> | undefined) ?? [];
|
|
138
142
|
return (
|
|
139
|
-
<section aria-label="Launch result" className={`space-y-2 rounded-md border border-
|
|
143
|
+
<section aria-label="Launch result" className={`space-y-2 rounded-md border border-positive-foreground/25 bg-positive p-4 ${className ?? ""}`}>
|
|
140
144
|
<h3 className="font-medium">Campaign {String(result.status ?? "launched")}</h3>
|
|
141
145
|
<p className="text-sm">{String(result.message ?? "")}</p>
|
|
142
146
|
<p className="text-sm text-muted-foreground">
|
|
143
147
|
Sequence {String(result.sequence_id ?? "")} with {String(result.steps_created ?? 0)} steps, sending from {channels.map((m) => m.email).join(", ") || "the chosen mailboxes"}.
|
|
144
148
|
</p>
|
|
145
|
-
{Array.isArray(result.warnings) && result.warnings.length > 0 ? <ul className="list-disc pl-5 text-sm text-
|
|
149
|
+
{Array.isArray(result.warnings) && result.warnings.length > 0 ? <ul className="list-disc pl-5 text-sm text-warning-foreground">{result.warnings.map((w) => <li key={String(w)}>{String(w)}</li>)}</ul> : null}
|
|
146
150
|
</section>
|
|
147
151
|
);
|
|
148
152
|
}
|
|
149
153
|
return (
|
|
150
154
|
<section aria-label="Launch" className={`space-y-3 ${className ?? ""}`}>
|
|
151
155
|
<h3 className="font-medium">Ready to launch {String(c.name ?? "")}</h3>
|
|
152
|
-
{alreadyLaunched ? <p role="status" className="rounded bg-
|
|
156
|
+
{alreadyLaunched ? <p role="status" className="rounded bg-positive p-2 text-sm text-positive-foreground">This campaign has already been launched. Manage it in the sequencer.</p> : null}
|
|
153
157
|
<ul className="space-y-1 text-sm">
|
|
154
158
|
<Check ok={hasAudience}>An audience list is attached</Check>
|
|
155
159
|
<Check ok={hasSteps}>The sequence has at least one step</Check>
|
|
@@ -157,23 +161,23 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
157
161
|
<Check ok={senders.size > 0}>At least one active sending mailbox is selected</Check>
|
|
158
162
|
</ul>
|
|
159
163
|
{genError && !copy.ready ? (
|
|
160
|
-
<p role="alert" className="rounded bg-
|
|
164
|
+
<p role="alert" className="rounded bg-error p-2 text-sm text-error-foreground">Writing the campaign documents failed: {genError}</p>
|
|
161
165
|
) : null}
|
|
162
166
|
{copy.known && !copy.ready ? (
|
|
163
167
|
<div className="space-y-1">
|
|
164
168
|
{writeCopy.error ? <ApiError error={writeCopy.error} title="Could not write the copy" /> : null}
|
|
165
|
-
{copyPoll.done && !copyStateOf(copyPoll.status).ready && !writeCopy.loading ? <p role="status" className="text-sm text-
|
|
166
|
-
{!copyPoll.polling && !copyPoll.done && copyPoll.attempts > 0 ? <p role="status" className="text-sm text-
|
|
169
|
+
{copyPoll.done && !copyStateOf(copyPoll.status).ready && !writeCopy.loading ? <p role="status" className="text-sm text-warning-foreground">The copy did not finish. Try again, or write it in Campaign assets.</p> : null}
|
|
170
|
+
{!copyPoll.polling && !copyPoll.done && copyPoll.attempts > 0 ? <p role="status" className="text-sm text-warning-foreground">Still writing. Check back in a minute.</p> : null}
|
|
167
171
|
<Button variant="outline" loading={writeCopy.loading} disabled={writingCopy} onClick={() => setConfirmCopy(true)}>Write the email copy with AI</Button>
|
|
168
172
|
</div>
|
|
169
173
|
) : null}
|
|
170
174
|
{unsafe ? (
|
|
171
|
-
<p role="alert" className="rounded bg-
|
|
175
|
+
<p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
172
176
|
Deliverability warning: {typeof v.reason === "string" ? v.reason : "sending is not recommended right now."}
|
|
173
177
|
</p>
|
|
174
178
|
) : null}
|
|
175
179
|
{held.length > 0 ? (
|
|
176
|
-
<div role="status" className="rounded bg-
|
|
180
|
+
<div role="status" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
177
181
|
<p>New mailboxes send nothing for their first {WARMUP_HOLD_DAYS} days while they warm up. The campaign goes live, but no email goes out from:</p>
|
|
178
182
|
<ul className="list-disc pl-5">{held.map(({ m, until }) => <li key={m.id}>{m.email ?? `Mailbox ${m.id}`} until {day(until as Date)}</li>)}</ul>
|
|
179
183
|
</div>
|
|
@@ -195,11 +199,12 @@ export function CampaignLaunchStep({ campaignId, audienceSize, onLaunched, class
|
|
|
195
199
|
{m.email ?? `Mailbox ${m.id}`}
|
|
196
200
|
</label>
|
|
197
201
|
))}
|
|
202
|
+
<FirstNotice shown={listOf<Mailbox>(mailboxes.data).length} limit={MAILBOX_LIMIT} noun="mailboxes" />
|
|
198
203
|
</fieldset>
|
|
199
204
|
{launch.error ? (
|
|
200
205
|
<div className="space-y-1">
|
|
201
206
|
<ApiError error={launch.error} title="The launch did not complete" />
|
|
202
|
-
<p className="text-sm text-
|
|
207
|
+
<p className="text-sm text-warning-foreground">Do not retry blindly. A sequence may already exist; check the campaign before launching again.</p>
|
|
203
208
|
</div>
|
|
204
209
|
) : null}
|
|
205
210
|
<Button disabled={!ready} loading={launch.loading} onClick={() => setConfirm(true)}>Launch campaign</Button>
|
|
@@ -40,7 +40,7 @@ function Tiles({ data }: { data: Record<string, unknown> }) {
|
|
|
40
40
|
{tiles.map(([k, v]) => (
|
|
41
41
|
<div key={k} className="rounded-md border p-3">
|
|
42
42
|
<dt className="text-xs capitalize text-muted-foreground">{k}</dt>
|
|
43
|
-
<dd className="text-xl font-
|
|
43
|
+
<dd className="text-xl font-medium">{v}</dd>
|
|
44
44
|
</div>
|
|
45
45
|
))}
|
|
46
46
|
</dl>
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { useEffect, useState } from "react";
|
|
27
27
|
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
28
28
|
import { ApiError } from "@/g8/components/api-error";
|
|
29
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
29
30
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
30
31
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
31
32
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -62,8 +63,11 @@ const emptyDraft: Draft = { name: "", category: "Outbound", goal: "", brief: "",
|
|
|
62
63
|
|
|
63
64
|
interface Idea { id?: string; campaign_name?: string; campaign_category?: string; core_concept?: string; why_this_works?: string; primary_hook?: string; target_channels?: string[] }
|
|
64
65
|
|
|
66
|
+
/** The persona API has no paging: a full read is announced under the checkboxes. */
|
|
67
|
+
const PERSONA_LIMIT = 50;
|
|
68
|
+
|
|
65
69
|
function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick: (i: Idea) => void; onSkip: () => void }) {
|
|
66
|
-
const personas = useG8Query((signal) => g8Get("/personas", { limit:
|
|
70
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), []);
|
|
67
71
|
const [goal, setGoal] = useState(GOALS[0]);
|
|
68
72
|
const [count, setCount] = useState(5);
|
|
69
73
|
const [direction, setDirection] = useState("");
|
|
@@ -104,6 +108,7 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
104
108
|
<input type="checkbox" checked={selected.includes(p.title ?? "")} onChange={(e) => setSelected(e.target.checked ? [...selected, p.title ?? ""] : selected.filter((t) => t !== p.title))} />{p.title}
|
|
105
109
|
</label>
|
|
106
110
|
))}
|
|
111
|
+
<FirstNotice shown={personaRows.length} limit={PERSONA_LIMIT} noun="personas" />
|
|
107
112
|
</fieldset>
|
|
108
113
|
) : null}
|
|
109
114
|
{[start.error, poll.error].map((e, i) => (e ? <ApiError key={i} error={e} title="Idea generation failed" /> : null))}
|
|
@@ -111,10 +116,10 @@ function IdeaStep({ websiteUrl, onPick, onSkip }: { websiteUrl: string; onPick:
|
|
|
111
116
|
<Button disabled={!websiteUrl || poll.polling} loading={start.loading} onClick={() => setConfirm(true)}>Generate ideas</Button>
|
|
112
117
|
<Button variant="outline" onClick={onSkip}>Skip, write my own</Button>
|
|
113
118
|
</div>
|
|
114
|
-
{!websiteUrl ? <p className="text-sm text-
|
|
119
|
+
{!websiteUrl ? <p className="text-sm text-warning-foreground">Save the company website in IcpEditor first; ideas are generated from it.</p> : null}
|
|
115
120
|
{poll.polling ? <p role="status" className="text-sm text-muted-foreground">Generating ideas. This takes 10 to 20 seconds.</p> : null}
|
|
116
|
-
{!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-
|
|
117
|
-
{failed ? <p role="alert" className="text-sm text-
|
|
121
|
+
{!poll.polling && !poll.done && poll.attempts > 0 && !poll.error ? <p role="status" className="text-sm text-warning-foreground">Still running. Check back in a minute and generate again if nothing appears.</p> : null}
|
|
122
|
+
{failed ? <p role="alert" className="text-sm text-error-foreground">Generation failed: {text(poll.status?.error) || "try again"}.</p> : null}
|
|
118
123
|
{poll.done && !failed && ideas.length === 0 ? <p className="text-sm text-muted-foreground">No ideas came back. Try a different goal or direction.</p> : null}
|
|
119
124
|
<ul className="space-y-2">
|
|
120
125
|
{ideas.map((idea, i) => (
|
|
@@ -206,7 +211,7 @@ export function CampaignWizard({ initialCampaignId, websiteUrl, onLaunched, auto
|
|
|
206
211
|
</nav>
|
|
207
212
|
|
|
208
213
|
{dispatchFailed ? (
|
|
209
|
-
<p role="alert" className="rounded bg-
|
|
214
|
+
<p role="alert" className="rounded bg-warning p-2 text-sm text-warning-foreground">
|
|
210
215
|
The campaign was created, but writing its documents could not be started. Write the email copy from the Launch step before launching.
|
|
211
216
|
</p>
|
|
212
217
|
) : null}
|
|
@@ -17,6 +17,7 @@ import { useState } from "react";
|
|
|
17
17
|
import type { ReactNode } from "react";
|
|
18
18
|
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
19
19
|
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
20
21
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
22
|
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
22
23
|
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
@@ -48,10 +49,13 @@ function isPinned(r: Record<string, unknown> | undefined): boolean {
|
|
|
48
49
|
return Boolean(r && (r.pinned ?? r.is_pinned));
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/** The ICP API has no paging: when the read is full, a missing ICP may just be beyond it, and the screen says so. */
|
|
53
|
+
const ICP_LIMIT = 200;
|
|
54
|
+
|
|
51
55
|
export function IcpDetail({ icpId, onClose, onExpand, onCollapse, onChanged, renderActions, className }: IcpDetailProps) {
|
|
52
56
|
const [form, setForm] = useState<Form | null>(null);
|
|
53
57
|
const [archiving, setArchiving] = useState(false);
|
|
54
|
-
const q = useG8Query((signal) => g8Get("/icps", { limit:
|
|
58
|
+
const q = useG8Query((signal) => g8Get("/icps", { limit: ICP_LIMIT }, { signal }), [icpId]);
|
|
55
59
|
const icp = listOf<Record<string, any>>(q.data).find((r) => String(r.id) === icpId);
|
|
56
60
|
|
|
57
61
|
const save = useG8Mutation(async (f: Form) => {
|
|
@@ -75,7 +79,12 @@ export function IcpDetail({ icpId, onClose, onExpand, onCollapse, onChanged, ren
|
|
|
75
79
|
|
|
76
80
|
if (q.error) return <ApiError error={q.error} title="Could not load the ICP" onRetry={() => void q.refetch()} className={className} />;
|
|
77
81
|
if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
78
|
-
if (!icp) return
|
|
82
|
+
if (!icp) return (
|
|
83
|
+
<div className={`p-4 text-sm ${className ?? ""}`}>
|
|
84
|
+
<p>This ICP no longer exists or was archived.</p>
|
|
85
|
+
<FirstNotice shown={listOf(q.data).length} limit={ICP_LIMIT} noun="ICPs" />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
79
88
|
|
|
80
89
|
const f = (icp.firmographics ?? {}) as Record<string, unknown>;
|
|
81
90
|
const arr = (v: unknown) => (Array.isArray(v) ? v.join(", ") : text(v));
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { useState } from "react";
|
|
22
22
|
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } 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";
|
|
@@ -108,8 +109,8 @@ function ProfileCard({ websiteUrl: override, onChange }: IcpEditorProps) {
|
|
|
108
109
|
</label>
|
|
109
110
|
) : null}
|
|
110
111
|
{status.polling ? <p role="status" className="text-sm text-muted-foreground">Building the profile. This can take a minute.</p> : null}
|
|
111
|
-
{status.done && !status.polling ? <p role="status" className="text-sm text-
|
|
112
|
-
{saved ? <p role="status" className="text-sm text-
|
|
112
|
+
{status.done && !status.polling ? <p role="status" className="text-sm text-positive-foreground">Profile build finished.</p> : null}
|
|
113
|
+
{saved ? <p role="status" className="text-sm text-positive-foreground">Saved.</p> : null}
|
|
113
114
|
{[save.error, generate.error, approve.error, status.error].map((e, i) => (e ? <ApiError key={i} error={e} /> : null))}
|
|
114
115
|
<div className="flex flex-wrap gap-2">
|
|
115
116
|
<Button loading={save.loading} disabled={!website.trim() || (approved && !unlock)} onClick={() => void save.mutate(undefined)}>Save profile</Button>
|
|
@@ -136,10 +137,13 @@ const emptyForm: IcpForm = { name: "", description: "", industries: "", size: ""
|
|
|
136
137
|
|
|
137
138
|
const formOf = (icp: IcpRow): IcpForm => icpFormOf(icp as Record<string, any>);
|
|
138
139
|
|
|
140
|
+
/** The ICP API has no paging: a full read is announced under the table. */
|
|
141
|
+
const ICP_LIMIT = 100;
|
|
142
|
+
|
|
139
143
|
export function IcpEditor(props: IcpEditorProps) {
|
|
140
144
|
const { onChange, onOpen, className } = props;
|
|
141
145
|
const profile = useCompanyProfile(props.websiteUrl);
|
|
142
|
-
const icps = useG8Query((signal) => g8Get("/icps", { limit:
|
|
146
|
+
const icps = useG8Query((signal) => g8Get("/icps", { limit: ICP_LIMIT }, { signal }), [props.refreshKey]);
|
|
143
147
|
const [editing, setEditing] = useState<IcpRow | "new" | null>(null);
|
|
144
148
|
const [form, setForm] = useState<IcpForm>(emptyForm);
|
|
145
149
|
const [archiving, setArchiving] = useState<IcpRow | null>(null);
|
|
@@ -192,6 +196,7 @@ export function IcpEditor(props: IcpEditorProps) {
|
|
|
192
196
|
))}
|
|
193
197
|
</TableBody>
|
|
194
198
|
</Table>
|
|
199
|
+
<FirstNotice shown={rows.length} limit={ICP_LIMIT} noun="ICPs" />
|
|
195
200
|
{!icps.loading && !icps.error && rows.length === 0 ? <EmptyState title="No ICPs yet" description="Describe who you sell to. Campaigns and personas build on it." /> : null}
|
|
196
201
|
</section>
|
|
197
202
|
|
|
@@ -16,6 +16,8 @@ import { useState } from "react";
|
|
|
16
16
|
import type { ReactNode } from "react";
|
|
17
17
|
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
18
18
|
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
20
|
+
import { hasNextOf } from "@/g8/lib/paging";
|
|
19
21
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
22
|
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
21
23
|
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
@@ -49,11 +51,18 @@ function isPinned(r: Record<string, unknown> | undefined): boolean {
|
|
|
49
51
|
return Boolean(r && (r.pinned ?? r.is_pinned));
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
/** Campaigns per read: every page is read, so no targeting campaign is missed. */
|
|
55
|
+
const CAMPAIGN_PAGE = 100;
|
|
56
|
+
|
|
52
57
|
export function PersonaDetail({ personaId, onClose, onExpand, onCollapse, onChanged, onOpenCampaign, renderActions, className }: PersonaDetailProps) {
|
|
53
58
|
const [form, setForm] = useState<Form | null>(null);
|
|
54
59
|
const [archiving, setArchiving] = useState(false);
|
|
55
60
|
const q = useG8Query((signal) => g8Get(`/personas/${personaId}`, undefined, { signal }), [personaId]);
|
|
56
|
-
const campaigns =
|
|
61
|
+
const campaigns = useAllPages<CampaignRow>(async (page, signal) => {
|
|
62
|
+
const env = await g8Get("/campaigns", { page, limit: CAMPAIGN_PAGE }, { signal });
|
|
63
|
+
const rows = listOf<CampaignRow>(env);
|
|
64
|
+
return { rows, hasNext: hasNextOf(env, CAMPAIGN_PAGE, rows.length) };
|
|
65
|
+
}, [personaId]);
|
|
57
66
|
const p = objectOf(q.data) as Record<string, any>;
|
|
58
67
|
|
|
59
68
|
const save = useG8Mutation(async (f: Form) => {
|
|
@@ -79,7 +88,7 @@ export function PersonaDetail({ personaId, onClose, onExpand, onCollapse, onChan
|
|
|
79
88
|
if (q.loading && !q.data) return <Skeleton className={`h-48 w-full ${className ?? ""}`} />;
|
|
80
89
|
|
|
81
90
|
const title = text(p.title) || `Persona ${personaId}`;
|
|
82
|
-
const targeting =
|
|
91
|
+
const targeting = campaigns.items.filter((c) => (c.target_persona ?? "").trim().toLowerCase() === title.trim().toLowerCase());
|
|
83
92
|
const startEdit = () => {
|
|
84
93
|
save.reset();
|
|
85
94
|
setForm(personaFormOf(p));
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { useState } from "react";
|
|
20
20
|
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
22
23
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
25
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -67,9 +68,12 @@ const emptyForm: PersonaForm = { title: "", why: "", signals: "", approach: "",
|
|
|
67
68
|
|
|
68
69
|
const formOf = (p: PersonaRow): PersonaForm => personaFormOf(p as Record<string, any>);
|
|
69
70
|
|
|
71
|
+
/** The persona API has no paging: a full read is announced under the table. */
|
|
72
|
+
const PERSONA_LIMIT = 100;
|
|
73
|
+
|
|
70
74
|
export function PersonaEditor({ websiteUrl, onChange, onOpen, refreshKey, className }: PersonaEditorProps) {
|
|
71
75
|
const profile = useCompanyProfile(websiteUrl);
|
|
72
|
-
const personas = useG8Query((signal) => g8Get("/personas", { limit:
|
|
76
|
+
const personas = useG8Query((signal) => g8Get("/personas", { limit: PERSONA_LIMIT }, { signal }), [refreshKey]);
|
|
73
77
|
const [editing, setEditing] = useState<PersonaRow | "new" | null>(null);
|
|
74
78
|
const [form, setForm] = useState<PersonaForm>(emptyForm);
|
|
75
79
|
const [archiving, setArchiving] = useState<PersonaRow | null>(null);
|
|
@@ -158,6 +162,7 @@ export function PersonaEditor({ websiteUrl, onChange, onOpen, refreshKey, classN
|
|
|
158
162
|
))}
|
|
159
163
|
</TableBody>
|
|
160
164
|
</Table>
|
|
165
|
+
<FirstNotice shown={rows.length} limit={PERSONA_LIMIT} noun="personas" />
|
|
161
166
|
{!personas.loading && !personas.error && rows.length === 0 ? <EmptyState title="No personas yet" description="Add the roles you sell to, or let AI recommend them." /> : null}
|
|
162
167
|
|
|
163
168
|
<Dialog open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }}>
|
|
@@ -98,10 +98,10 @@ export function commaList(value: string): string[] {
|
|
|
98
98
|
|
|
99
99
|
export function statusClass(status: string | null | undefined): string {
|
|
100
100
|
const s = (status ?? "").toLowerCase();
|
|
101
|
-
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-
|
|
102
|
-
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-
|
|
103
|
-
if (s) return "bg-
|
|
104
|
-
return "bg-
|
|
101
|
+
if (["active", "live", "completed", "approved", "complete", "done"].includes(s)) return "bg-positive text-positive-foreground";
|
|
102
|
+
if (["draft", "pending", "running", "generating", "in_progress", "copy_in_progress", "scheduling"].includes(s)) return "bg-warning text-warning-foreground";
|
|
103
|
+
if (s) return "bg-error text-error-foreground";
|
|
104
|
+
return "bg-muted text-foreground";
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/** Timestamps are shown in UTC so the same value reads the same everywhere. */
|