@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -23,14 +23,21 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
|
23
23
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
24
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
25
|
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
27
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
28
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
26
29
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
30
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
28
31
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
32
|
import { arrayOf, formatWhen } from "../lib/integrations-utils";
|
|
33
|
+
import { plural } from "@/g8/lib/plural";
|
|
34
|
+
import { DEFAULT_BRAND, useBrand } from "@/g8/hooks/use-brand";
|
|
30
35
|
|
|
31
36
|
export interface WebhookManagerProps {
|
|
32
37
|
/** Pre-fill the endpoint URL of the create form (your /api/webhooks/graph8 route). */
|
|
33
38
|
defaultUrl?: string;
|
|
39
|
+
/** Called when a person opens a webhook (show a WebhookDetail). */
|
|
40
|
+
onSelectWebhook?: (webhook: { id: string; url: string; name?: string | null }) => void;
|
|
34
41
|
className?: string;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -65,13 +72,13 @@ type Pending =
|
|
|
65
72
|
| { kind: "rotate"; hook: Webhook }
|
|
66
73
|
| { kind: "delete"; hook: Webhook };
|
|
67
74
|
|
|
68
|
-
function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: string; label: string } {
|
|
75
|
+
function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier: ConfirmTier; title: string; description: string; label: string } {
|
|
69
76
|
switch (p.kind) {
|
|
70
77
|
case "create":
|
|
71
78
|
return {
|
|
72
79
|
tier: "external",
|
|
73
|
-
title: `Send ${p.events.length
|
|
74
|
-
description:
|
|
80
|
+
title: `Send ${plural(p.events.length, "event type")} to ${p.url}`,
|
|
81
|
+
description: `${platform} will POST signed events to this URL as they happen.`,
|
|
75
82
|
label: "Create webhook",
|
|
76
83
|
};
|
|
77
84
|
case "toggle":
|
|
@@ -80,7 +87,7 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
|
|
|
80
87
|
title: `${p.hook.is_active === false ? "Resume" : "Pause"} deliveries to ${p.hook.url}`,
|
|
81
88
|
description:
|
|
82
89
|
p.hook.is_active === false
|
|
83
|
-
?
|
|
90
|
+
? `${platform} starts sending events to this URL again.`
|
|
84
91
|
: "Events stop being sent to this URL until you resume.",
|
|
85
92
|
label: p.hook.is_active === false ? "Resume" : "Pause",
|
|
86
93
|
};
|
|
@@ -101,16 +108,24 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
|
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
/** Deliveries per read; "Load more" appends the next page. */
|
|
112
|
+
const DELIVERY_PAGE = 20;
|
|
113
|
+
|
|
104
114
|
function Deliveries({ hook }: { hook: Webhook }) {
|
|
105
|
-
const list =
|
|
106
|
-
|
|
115
|
+
const list = useLoadMoreList(byPage<Delivery>(DELIVERY_PAGE, async (page, signal) => {
|
|
116
|
+
const env = await g8Get(`/webhooks/${hook.id}/deliveries`, { page, limit: DELIVERY_PAGE }, { signal });
|
|
117
|
+
const rows = arrayOf<Delivery>(env);
|
|
118
|
+
return { rows, hasNext: hasNextOf(env, DELIVERY_PAGE, rows.length) };
|
|
119
|
+
}), [hook.id]);
|
|
120
|
+
if (list.loading && !list.data) return <Skeleton className="h-16 w-full" />;
|
|
107
121
|
if (list.error) return <ApiError error={list.error} onRetry={() => void list.refetch()} />;
|
|
108
|
-
const rows =
|
|
109
|
-
if (rows.length === 0) return <p className="text-sm text-
|
|
122
|
+
const rows = list.data ?? [];
|
|
123
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No deliveries yet.</p>;
|
|
110
124
|
return (
|
|
125
|
+
<>
|
|
111
126
|
<table className="w-full text-sm" aria-label="Deliveries">
|
|
112
127
|
<thead>
|
|
113
|
-
<tr className="text-left text-
|
|
128
|
+
<tr className="text-left text-muted-foreground">
|
|
114
129
|
<th>Event</th>
|
|
115
130
|
<th>Status</th>
|
|
116
131
|
<th>Code</th>
|
|
@@ -130,10 +145,13 @@ function Deliveries({ hook }: { hook: Webhook }) {
|
|
|
130
145
|
))}
|
|
131
146
|
</tbody>
|
|
132
147
|
</table>
|
|
148
|
+
<LoadMore shown={rows.length} noun="deliveries" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
149
|
+
</>
|
|
133
150
|
);
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
export function WebhookManager({ defaultUrl = "", className }: WebhookManagerProps) {
|
|
153
|
+
export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }: WebhookManagerProps) {
|
|
154
|
+
const brand = useBrand();
|
|
137
155
|
const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
|
|
138
156
|
const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
|
|
139
157
|
const [url, setUrl] = useState(defaultUrl);
|
|
@@ -169,29 +187,29 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
169
187
|
);
|
|
170
188
|
const urlOk = /^https:\/\/\S+$/.test(url.trim());
|
|
171
189
|
const toggle = (event: string) => setChosen((c) => (c.includes(event) ? c.filter((x) => x !== event) : [...c, event]));
|
|
172
|
-
const confirm = pending ? copyFor(pending) : null;
|
|
190
|
+
const confirm = pending ? copyFor(pending, brand.linkLabel) : null;
|
|
173
191
|
|
|
174
192
|
return (
|
|
175
193
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
176
|
-
<section className="space-y-3 rounded border border-
|
|
177
|
-
<h3 className="text-sm font-
|
|
194
|
+
<section className="space-y-3 rounded border border-border p-3" aria-label="New webhook">
|
|
195
|
+
<h3 className="text-sm font-medium">New webhook</h3>
|
|
178
196
|
<div className="grid gap-2 md:grid-cols-2">
|
|
179
197
|
<Input
|
|
180
198
|
aria-label="Endpoint URL"
|
|
181
|
-
placeholder=
|
|
199
|
+
placeholder={`https://yourapp.com/api/webhooks/${brand.linkLabel.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`}
|
|
182
200
|
value={url}
|
|
183
201
|
onChange={(e) => setUrl(e.target.value)}
|
|
184
202
|
/>
|
|
185
203
|
<Input aria-label="Name" placeholder="Name (optional)" value={name} onChange={(e) => setName(e.target.value)} />
|
|
186
204
|
</div>
|
|
187
|
-
{url && !urlOk ? <p className="text-xs text-
|
|
205
|
+
{url && !urlOk ? <p className="text-xs text-error-foreground">The endpoint must be an https:// URL.</p> : null}
|
|
188
206
|
{events.loading ? (
|
|
189
207
|
<Skeleton className="h-12 w-full" />
|
|
190
208
|
) : (
|
|
191
209
|
<div className="space-y-2">
|
|
192
210
|
{Object.entries(byCategory).map(([category, items]) => (
|
|
193
211
|
<fieldset key={category}>
|
|
194
|
-
<legend className="text-xs font-medium uppercase text-
|
|
212
|
+
<legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
|
|
195
213
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
196
214
|
{items.map((e) => (
|
|
197
215
|
<label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}>
|
|
@@ -213,10 +231,10 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
213
231
|
</section>
|
|
214
232
|
|
|
215
233
|
{secret ? (
|
|
216
|
-
<div role="alert" className="rounded border border-
|
|
234
|
+
<div role="alert" className="rounded border border-warning-foreground/30 bg-warning p-3 text-sm">
|
|
217
235
|
<p className="font-medium">Signing secret for {secret.url}. It is shown once.</p>
|
|
218
|
-
<code className="mt-1 block break-all rounded bg-
|
|
219
|
-
<p className="mt-1 text-xs text-
|
|
236
|
+
<code className="mt-1 block break-all rounded bg-background p-2">{secret.value}</code>
|
|
237
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
220
238
|
Store it in your server environment as G8_WEBHOOK_SECRET. Never put it in browser code.
|
|
221
239
|
</p>
|
|
222
240
|
<Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>
|
|
@@ -233,13 +251,13 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
233
251
|
) : list.length === 0 ? (
|
|
234
252
|
<EmptyState title="No webhooks yet" description="Create one above to receive events like replies and meetings on your server." />
|
|
235
253
|
) : (
|
|
236
|
-
<ul className="divide-y rounded border border-
|
|
254
|
+
<ul className="divide-y rounded border border-border">
|
|
237
255
|
{list.map((h) => (
|
|
238
256
|
<li key={h.id} className="space-y-2 px-3 py-3">
|
|
239
257
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
240
258
|
<div>
|
|
241
|
-
<div className="font-medium">{h.name || h.url}</div>
|
|
242
|
-
<div className="text-xs text-
|
|
259
|
+
{onSelectWebhook ? <button type="button" className="font-medium hover:underline" onClick={() => onSelectWebhook(h)}>{h.name || h.url}</button> : <div className="font-medium">{h.name || h.url}</div>}
|
|
260
|
+
<div className="text-xs text-muted-foreground">
|
|
243
261
|
{h.name ? `${h.url} - ` : ""}
|
|
244
262
|
{(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}
|
|
245
263
|
</div>
|
|
@@ -270,12 +288,12 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
270
288
|
</ul>
|
|
271
289
|
)}
|
|
272
290
|
|
|
273
|
-
<details className="rounded border border-
|
|
291
|
+
<details className="rounded border border-border p-3 text-sm">
|
|
274
292
|
<summary className="cursor-pointer font-medium">Verify deliveries (server side)</summary>
|
|
275
|
-
<p className="mt-2 text-
|
|
293
|
+
<p className="mt-2 text-muted-foreground">
|
|
276
294
|
Check the signature on the raw body before trusting an event. Deliveries repeat, so deduplicate on the event id.
|
|
277
295
|
</p>
|
|
278
|
-
<pre className="mt-2 overflow-x-auto rounded bg-
|
|
296
|
+
<pre className="mt-2 overflow-x-auto rounded bg-muted p-3 text-xs text-foreground">
|
|
279
297
|
<code>{`const event = g8.webhooks.constructEvent(rawBody,
|
|
280
298
|
req.headers.get("x-studio-signature") ?? "",
|
|
281
299
|
req.headers.get("x-studio-timestamp") ?? "",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WorkspaceTools: Slack, Google Workspace and Roam, each as a card with its live status. They connect through a sign-in screen that only runs
|
|
5
|
+
* in the graph8 app, so a tool that is not connected shows a "Connect in graph8" link, never a dead button. Connected tools show what the
|
|
6
|
+
* status reports. Google Workspace also has "Test connection" (a live check) and a Drive browser (folders one level at a time with their
|
|
7
|
+
* files; "Load more" follows Google's page token) for confirming which folders graph8 can read.
|
|
8
|
+
* All reads. Nothing here changes a connection.
|
|
9
|
+
*
|
|
10
|
+
* SDK operations, in order:
|
|
11
|
+
* 1. getSlackStatus / getGoogleWorkspaceStatus / getRoamStatus GET /integrations/slack/status, /integrations/google-workspace, /integrations/roam/status (read)
|
|
12
|
+
* 2. testGoogleWorkspaceConnection POST /integrations/google-workspace/test (read)
|
|
13
|
+
* 3. listGoogleDriveFolders GET /integrations/google-workspace/folders?parent_id&view&page_size&page_token (read)
|
|
14
|
+
* 4. listGoogleDriveFolderFiles GET /integrations/google-workspace/folders/{folder_id}/files?page_size&page_token (read)
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
import { Folder } from "lucide-react";
|
|
18
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
21
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
26
|
+
import { FactList } from "../lib/facts";
|
|
27
|
+
import { GRAPH8_APP_INTEGRATIONS_PATH, looksConnected } from "../lib/integrations-utils";
|
|
28
|
+
|
|
29
|
+
export interface WorkspaceToolsProps {
|
|
30
|
+
/** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
|
|
31
|
+
connectUrl?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DriveFolder { id: string; name: string }
|
|
36
|
+
interface DriveFile { id: string; name: string; mimeType?: string; size?: string | number | null }
|
|
37
|
+
const TOOLS = [
|
|
38
|
+
{ key: "slack", label: "Slack", path: "/integrations/slack/status", note: "Send alerts and notifications to Slack channels." },
|
|
39
|
+
{ key: "google", label: "Google Workspace", path: "/integrations/google-workspace", note: "Read Drive folders and use your Workspace account." },
|
|
40
|
+
{ key: "roam", label: "Roam", path: "/integrations/roam/status", note: "Post to Roam groups and users." },
|
|
41
|
+
] as const;
|
|
42
|
+
const enc = encodeURIComponent;
|
|
43
|
+
type Query = { data: unknown; error: unknown; loading: boolean; refetch: () => Promise<unknown> };
|
|
44
|
+
|
|
45
|
+
/** One paged Drive list. Folders and files are separate queries, so each keeps its own page token (a token only works for the query that issued it). */
|
|
46
|
+
function usePagedDrive<T>(path: string, key: string, field: "folders" | "files", extraQuery: Record<string, string | number>, enabled: boolean) {
|
|
47
|
+
const [acc, setAcc] = useState<T[]>([]);
|
|
48
|
+
const [token, setToken] = useState<string | undefined>();
|
|
49
|
+
const [next, setNext] = useState<string | undefined>();
|
|
50
|
+
useEffect(() => { setAcc([]); setToken(undefined); setNext(undefined); }, [key]);
|
|
51
|
+
const q = useG8Query((signal) => g8Get(path, { ...extraQuery, page_size: 50, page_token: token }, { signal }), [key, token], { enabled });
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const d = (q.data as { data?: Record<string, unknown> } | undefined)?.data;
|
|
54
|
+
if (!d || !Array.isArray(d[field])) return;
|
|
55
|
+
setAcc((prev) => (token ? [...prev, ...(d[field] as T[])] : (d[field] as T[])));
|
|
56
|
+
setNext((d.next_page_token as string | null | undefined) ?? undefined);
|
|
57
|
+
}, [q.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
58
|
+
return { items: acc, next, loading: q.loading, error: q.error, refetch: q.refetch, more: () => next && setToken(next) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function DriveBrowser() {
|
|
62
|
+
const [trail, setTrail] = useState<DriveFolder[]>([]);
|
|
63
|
+
const parent = trail[trail.length - 1]?.id ?? "root";
|
|
64
|
+
const folders = usePagedDrive<DriveFolder>("/integrations/google-workspace/folders", parent, "folders", { parent_id: parent, view: "my_drive" }, true);
|
|
65
|
+
const files = usePagedDrive<DriveFile>(`/integrations/google-workspace/folders/${enc(parent)}/files`, parent, "files", {}, parent !== "root");
|
|
66
|
+
const go = (t: DriveFolder[]) => setTrail(t);
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-col gap-2">
|
|
69
|
+
<nav aria-label="Folder path" className="flex flex-wrap items-center gap-1 text-sm">
|
|
70
|
+
<button type="button" className="underline" onClick={() => go([])}>My Drive</button>
|
|
71
|
+
{trail.map((f, i) => <span key={f.id}> / <button type="button" className="underline" onClick={() => go(trail.slice(0, i + 1))}>{f.name}</button></span>)}
|
|
72
|
+
</nav>
|
|
73
|
+
{folders.error ? <ApiError error={folders.error} title="Could not list the folders" onRetry={() => void folders.refetch()} /> : null}
|
|
74
|
+
{(folders.loading && folders.items.length === 0) ? <Skeleton className="h-10 w-full" /> : null}
|
|
75
|
+
<ul className="divide-y rounded-md border text-sm" aria-label="Folders">
|
|
76
|
+
{folders.items.map((f) => <li key={f.id}><button type="button" className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-accent" onClick={() => go([...trail, f])}><Folder className="size-4" aria-hidden />{f.name}</button></li>)}
|
|
77
|
+
</ul>
|
|
78
|
+
{folders.next ? <Button size="sm" variant="outline" className="self-start" loading={folders.loading} onClick={folders.more}>Load more folders</Button> : null}
|
|
79
|
+
{parent !== "root" ? (
|
|
80
|
+
<>
|
|
81
|
+
{files.error ? <ApiError compact error={files.error} title="Could not list the files" onRetry={() => void files.refetch()} /> : null}
|
|
82
|
+
<ul className="divide-y rounded-md border text-sm" aria-label="Files">
|
|
83
|
+
{files.items.map((f) => <li key={f.id} className="flex justify-between px-3 py-1.5"><span>{f.name}</span><span className="text-xs text-muted-foreground">{f.mimeType?.split(".").pop()}</span></li>)}
|
|
84
|
+
</ul>
|
|
85
|
+
{files.next ? <Button size="sm" variant="outline" className="self-start" loading={files.loading} onClick={files.more}>Load more files</Button> : null}
|
|
86
|
+
</>
|
|
87
|
+
) : null}
|
|
88
|
+
{!folders.loading && !folders.error && folders.items.length === 0 && (parent === "root" || (!files.loading && files.items.length === 0)) ? <p className="text-sm text-muted-foreground">This folder is empty.</p> : null}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ToolCard({ tool, connectUrl }: { tool: (typeof TOOLS)[number]; connectUrl: string }) {
|
|
94
|
+
const brand = useBrand();
|
|
95
|
+
const [drive, setDrive] = useState(false);
|
|
96
|
+
const status = useG8Query((signal) => g8Get(tool.path, undefined, { signal }), [tool.path]);
|
|
97
|
+
const test = useG8Mutation(() => g8Post("/integrations/google-workspace/test", {}));
|
|
98
|
+
const connected = looksConnected(status.data);
|
|
99
|
+
const verdict = (test.data as { data?: { success?: boolean; ok?: boolean; message?: string; error?: string } } | undefined)?.data;
|
|
100
|
+
const body = ((status.data as { data?: Record<string, unknown> } | undefined)?.data ?? {}) as Record<string, unknown>;
|
|
101
|
+
const facts = Object.fromEntries(Object.entries(body).filter(([k]) => !["connected", "is_connected"].includes(k)));
|
|
102
|
+
return (
|
|
103
|
+
<SectionCard title={tool.label} description={tool.note} actions={status.data ? <StatusPill tone={connected ? "positive" : "neutral"} dot>{connected ? "Connected" : "Not connected"}</StatusPill> : undefined}>
|
|
104
|
+
{status.error ? <ApiError error={status.error} title={`Could not check ${tool.label}`} onRetry={() => void status.refetch()} /> : null}
|
|
105
|
+
{status.loading && !status.data ? <Skeleton className="h-10 w-full" /> : null}
|
|
106
|
+
{status.data && !connected ? <a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">Connect {tool.label} in {brand.linkLabel}</a> : null}
|
|
107
|
+
{connected ? <FactList data={facts} empty="Connected." /> : null}
|
|
108
|
+
{connected && tool.key === "google" ? (
|
|
109
|
+
<div className="mt-3 flex flex-col gap-2">
|
|
110
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
111
|
+
<Button size="sm" variant="outline" loading={test.loading} onClick={() => void test.mutate(undefined)}>Test connection</Button>
|
|
112
|
+
<Button size="sm" variant="outline" aria-expanded={drive} onClick={() => setDrive((v) => !v)}>{drive ? "Hide Drive" : "Browse Drive"}</Button>
|
|
113
|
+
{test.data !== undefined ? <span role="status" className="text-sm" data-testid="google-test">{verdict?.success === false || verdict?.ok === false ? `The test failed${verdict.message || verdict.error ? `: ${verdict.message ?? verdict.error}` : ""}` : "Google answered: the connection works."}</span> : null}
|
|
114
|
+
</div>
|
|
115
|
+
{test.error ? <ApiError error={test.error} title="The test could not run" /> : null}
|
|
116
|
+
{drive ? <DriveBrowser /> : null}
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
</SectionCard>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function WorkspaceTools({ connectUrl: connectUrlProp, className }: WorkspaceToolsProps) {
|
|
124
|
+
const brand = useBrand();
|
|
125
|
+
const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
|
|
126
|
+
return <div className={`grid gap-4 md:grid-cols-2 ${className ?? ""}`}>{TOOLS.map((t) => <ToolCard key={t.key} tool={t} connectUrl={connectUrl} />)}</div>;
|
|
127
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/** Helpers for reading records that come back in a CRM's own shape (HubSpot properties, Salesforce fields, Pipedrive values). */
|
|
4
|
+
|
|
5
|
+
export type CrmRecord = Record<string, unknown> & { id?: string | number; properties?: Record<string, unknown> };
|
|
6
|
+
|
|
7
|
+
const pick = (r: CrmRecord, keys: string[]): string => {
|
|
8
|
+
const sources: Array<Record<string, unknown>> = [r, (r.properties ?? {}) as Record<string, unknown>, ((r as { attributes?: Record<string, unknown> }).attributes ?? {}) as Record<string, unknown>];
|
|
9
|
+
for (const src of sources) for (const k of keys) { const v = src[k]; if (typeof v === "string" && v.trim()) return v; if (typeof v === "number") return String(v); }
|
|
10
|
+
return "";
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** A readable name for a CRM record of any kind. */
|
|
14
|
+
export function crmRecordTitle(r: CrmRecord): string {
|
|
15
|
+
const first = pick(r, ["firstname", "first_name", "FirstName"]);
|
|
16
|
+
const last = pick(r, ["lastname", "last_name", "LastName"]);
|
|
17
|
+
return pick(r, ["name", "full_name", "Name", "dealname", "title", "subject"]) || [first, last].filter(Boolean).join(" ") || pick(r, ["email", "Email", "domain", "company"]) || String(r.id ?? "Record");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function crmRecordSubtitle(r: CrmRecord): string {
|
|
21
|
+
return [pick(r, ["email", "Email"]), pick(r, ["company", "Company", "domain"]), pick(r, ["stage", "dealstage", "StageName", "status", "Status"])].filter(Boolean).join(" - ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The record's own scalar fields (properties flattened in), for a detail view. Objects and empty values are left out. */
|
|
25
|
+
export function flattenRecord(r: CrmRecord): Record<string, string> {
|
|
26
|
+
const out: Record<string, string> = {};
|
|
27
|
+
const add = (src: Record<string, unknown>) => { for (const [k, v] of Object.entries(src)) if ((typeof v === "string" && v) || typeof v === "number" || typeof v === "boolean") out[k] = String(v); };
|
|
28
|
+
add(r);
|
|
29
|
+
add((r.properties ?? {}) as Record<string, unknown>);
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const CRM_ENTITIES = [
|
|
34
|
+
{ id: "contacts", label: "Contacts" },
|
|
35
|
+
{ id: "companies", label: "Companies" },
|
|
36
|
+
{ id: "deals", label: "Deals" },
|
|
37
|
+
{ id: "leads", label: "Leads" },
|
|
38
|
+
] as const;
|
|
39
|
+
export type CrmEntity = (typeof CRM_ENTITIES)[number]["id"];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/** Renders an open-shaped API object (health, scorecard, status) as labelled facts without guessing its fields. */
|
|
4
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
5
|
+
|
|
6
|
+
const label = (k: string): string => { const t = k.replace(/_/g, " "); return t.charAt(0).toUpperCase() + t.slice(1); };
|
|
7
|
+
const show = (v: unknown): string => (v == null ? "" : typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v));
|
|
8
|
+
|
|
9
|
+
export function FactList({ data, empty = "Nothing reported yet." }: { data: unknown; empty?: string }) {
|
|
10
|
+
const entries = Object.entries((data && typeof data === "object" && !Array.isArray(data) ? data : {}) as Record<string, unknown>).filter(([, v]) => v != null && v !== "");
|
|
11
|
+
if (entries.length === 0) return <p className="text-sm text-muted-foreground">{empty}</p>;
|
|
12
|
+
return <KeyValueList columns={2} items={entries.map(([k, v]) => ({ label: label(k), value: show(v), wide: typeof v === "object", mono: typeof v === "object" }))} />;
|
|
13
|
+
}
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
/** Types and helpers shared by the integrations components. */
|
|
4
4
|
|
|
5
|
+
import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
|
|
6
|
+
|
|
5
7
|
export const CRM_PROVIDERS = ["hubspot", "salesforce", "pipedrive", "zoho", "sugarcrm"] as const;
|
|
6
8
|
export type CrmProvider = (typeof CRM_PROVIDERS)[number];
|
|
7
9
|
|
|
10
|
+
/** The connection type the API expects (`CrmConnectionType`): Zoho is `zoho-crm`, the others are their own id. */
|
|
11
|
+
export function crmType(provider: string): string {
|
|
12
|
+
return provider === "zoho" ? "zoho-crm" : provider;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export const PROVIDER_LABELS: Record<CrmProvider, string> = {
|
|
9
16
|
hubspot: "HubSpot",
|
|
10
17
|
salesforce: "Salesforce",
|
|
@@ -14,7 +21,10 @@ export const PROVIDER_LABELS: Record<CrmProvider, string> = {
|
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
/** Where the person completes an OAuth connection. The API has no way to run the consent screen for you. */
|
|
17
|
-
|
|
24
|
+
/** The path on the platform's web app; components resolve it against the brand (`brandUrl(useBrand(), ...)`). */
|
|
25
|
+
export const GRAPH8_APP_INTEGRATIONS_PATH = "/studio/settings?tab=integrations";
|
|
26
|
+
/** The default (graph8) URL. Kept for existing imports; prefer GRAPH8_APP_INTEGRATIONS_PATH with useBrand. */
|
|
27
|
+
export const GRAPH8_APP_INTEGRATIONS_URL = `${DEFAULT_BRAND.appUrl}${GRAPH8_APP_INTEGRATIONS_PATH}`;
|
|
18
28
|
|
|
19
29
|
export interface CrmSyncRow {
|
|
20
30
|
provider: string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Paging helpers for the agents block, in the same shape as the foundation's paging primitives (a list never stops silently at a
|
|
5
|
+
* cap: it shows "Load more" while the last page was full).
|
|
6
|
+
*
|
|
7
|
+
* useLoadMoreList offset + limit APIs (the next page starts at items.length).
|
|
8
|
+
* useGrowingList limit-only APIs (a bigger limit refetches the list from the start).
|
|
9
|
+
* LoadMore the footer button plus the error of a failed "more" request.
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
import { Button } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
|
|
15
|
+
export interface LoadMoreList<T> {
|
|
16
|
+
items: T[];
|
|
17
|
+
/** The first page is loading (or reloading after the deps changed). */
|
|
18
|
+
loading: boolean;
|
|
19
|
+
loadingMore: boolean;
|
|
20
|
+
error: unknown;
|
|
21
|
+
moreError: unknown;
|
|
22
|
+
hasMore: boolean;
|
|
23
|
+
/** A limit-only API stopped at its own maximum: there are more rows the API will not return. */
|
|
24
|
+
capped?: boolean;
|
|
25
|
+
loadMore: () => void;
|
|
26
|
+
reload: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Options {
|
|
30
|
+
pageSize?: number;
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
/** useGrowingList: the largest `limit` the API accepts. */
|
|
33
|
+
max?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Offset paging. `fetchPage(offset, limit)` returns that page's rows; a full page means there may be more. */
|
|
37
|
+
export function useLoadMoreList<T>(
|
|
38
|
+
fetchPage: (offset: number, limit: number, signal: AbortSignal) => Promise<T[]>,
|
|
39
|
+
deps: readonly unknown[],
|
|
40
|
+
{ pageSize = 50, enabled = true }: Options = {},
|
|
41
|
+
): LoadMoreList<T> {
|
|
42
|
+
const [items, setItems] = useState<T[]>([]);
|
|
43
|
+
const [loading, setLoading] = useState(enabled);
|
|
44
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
45
|
+
const [error, setError] = useState<unknown>(null);
|
|
46
|
+
const [moreError, setMoreError] = useState<unknown>(null);
|
|
47
|
+
const [hasMore, setHasMore] = useState(false);
|
|
48
|
+
const [tick, setTick] = useState(0);
|
|
49
|
+
const fetchRef = useRef(fetchPage);
|
|
50
|
+
fetchRef.current = fetchPage;
|
|
51
|
+
const gen = useRef(0);
|
|
52
|
+
const count = useRef(0);
|
|
53
|
+
const sig = JSON.stringify([...deps, enabled, pageSize]);
|
|
54
|
+
const lastSig = useRef(sig);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!enabled) { setLoading(false); return; }
|
|
58
|
+
const mine = ++gen.current;
|
|
59
|
+
const ctl = new AbortController();
|
|
60
|
+
// A reload() keeps showing the rows it has until the fresh first page lands; a changed filter starts from nothing.
|
|
61
|
+
const fresh = lastSig.current !== sig;
|
|
62
|
+
lastSig.current = sig;
|
|
63
|
+
setLoading(true); setError(null); setMoreError(null);
|
|
64
|
+
if (fresh) { setItems([]); setHasMore(false); count.current = 0; }
|
|
65
|
+
// A reload() refetches as many pages as were showing, so a row opened from page 2 does not vanish; a changed filter starts at page 1.
|
|
66
|
+
const want = fresh ? pageSize : Math.max(count.current, pageSize);
|
|
67
|
+
const load = async (): Promise<T[]> => {
|
|
68
|
+
const all: T[] = [];
|
|
69
|
+
let last = pageSize;
|
|
70
|
+
while (all.length < want && last >= pageSize) {
|
|
71
|
+
const page = await fetchRef.current(all.length, pageSize, ctl.signal);
|
|
72
|
+
all.push(...page);
|
|
73
|
+
last = page.length;
|
|
74
|
+
}
|
|
75
|
+
return all;
|
|
76
|
+
};
|
|
77
|
+
load().then((rows) => {
|
|
78
|
+
if (gen.current !== mine) return;
|
|
79
|
+
setItems(rows); count.current = rows.length; setHasMore(rows.length > 0 && rows.length % pageSize === 0); setLoading(false);
|
|
80
|
+
}, (e) => { if (gen.current === mine) { setError(e); setLoading(false); } });
|
|
81
|
+
return () => ctl.abort();
|
|
82
|
+
}, [...deps, enabled, pageSize, tick]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
83
|
+
|
|
84
|
+
const loadMore = useCallback(() => {
|
|
85
|
+
const mine = gen.current;
|
|
86
|
+
setLoadingMore(true); setMoreError(null);
|
|
87
|
+
fetchRef.current(count.current, pageSize, new AbortController().signal).then((rows) => {
|
|
88
|
+
if (gen.current !== mine) return;
|
|
89
|
+
count.current += rows.length;
|
|
90
|
+
setItems((prev) => [...prev, ...rows]); setHasMore(rows.length >= pageSize); setLoadingMore(false);
|
|
91
|
+
}, (e) => { if (gen.current === mine) { setMoreError(e); setLoadingMore(false); } });
|
|
92
|
+
}, [pageSize]);
|
|
93
|
+
|
|
94
|
+
return { items, loading, loadingMore, error, moreError, hasMore, loadMore, reload: () => setTick((t) => t + 1) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Limit-only APIs: asks for `step` rows, and "Load more" asks again for `step` more from the start. */
|
|
98
|
+
export function useGrowingList<T>(
|
|
99
|
+
fetchRows: (limit: number, signal: AbortSignal) => Promise<T[]>,
|
|
100
|
+
deps: readonly unknown[],
|
|
101
|
+
{ pageSize = 50, enabled = true, max = Infinity }: Options = {},
|
|
102
|
+
): LoadMoreList<T> {
|
|
103
|
+
const [limit, setLimit] = useState(pageSize);
|
|
104
|
+
const [rows, setRows] = useState<T[]>([]);
|
|
105
|
+
const [loading, setLoading] = useState(enabled);
|
|
106
|
+
const [error, setError] = useState<unknown>(null);
|
|
107
|
+
const [tick, setTick] = useState(0);
|
|
108
|
+
const [growing, setGrowing] = useState(false);
|
|
109
|
+
const fetchRef = useRef(fetchRows);
|
|
110
|
+
fetchRef.current = fetchRows;
|
|
111
|
+
const depKey = JSON.stringify(deps);
|
|
112
|
+
const lastDeps = useRef(depKey);
|
|
113
|
+
|
|
114
|
+
useEffect(() => { if (lastDeps.current !== depKey) { lastDeps.current = depKey; setLimit(pageSize); } }, [depKey, pageSize]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!enabled) { setLoading(false); return; }
|
|
117
|
+
let live = true;
|
|
118
|
+
const ctl = new AbortController();
|
|
119
|
+
setLoading(true); setError(null);
|
|
120
|
+
fetchRef.current(limit, ctl.signal).then((r) => { if (live) { setRows(r); setLoading(false); setGrowing(false); } }, (e) => { if (live) { setError(e); setLoading(false); setGrowing(false); } });
|
|
121
|
+
return () => { live = false; ctl.abort(); };
|
|
122
|
+
}, [limit, depKey, enabled, tick]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
items: rows, loading: loading && !growing, loadingMore: loading && growing, error: growing ? null : error, moreError: growing ? error : null,
|
|
126
|
+
hasMore: !loading && rows.length >= limit && limit < max, capped: !loading && rows.length >= limit && limit >= max,
|
|
127
|
+
loadMore: () => { setGrowing(true); setLimit((l) => Math.min(max, l + pageSize)); }, reload: () => setTick((t) => t + 1),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The footer of a paged list. Renders nothing when everything is shown. */
|
|
132
|
+
export function LoadMore<T>({ list, noun = "rows" }: { list: LoadMoreList<T>; noun?: string }) {
|
|
133
|
+
if (!list.hasMore && !list.moreError && !list.capped) return null;
|
|
134
|
+
return (
|
|
135
|
+
<div className="flex flex-col items-center gap-2 py-2">
|
|
136
|
+
{list.moreError ? <ApiError compact error={list.moreError} title={`Could not load more ${noun}`} onRetry={list.loadMore} /> : null}
|
|
137
|
+
{list.capped ? <p className="text-xs text-muted-foreground">Showing the latest {list.items.length} {noun}. The API returns no more than this.</p> : null}
|
|
138
|
+
{list.hasMore && !list.moreError ? <Button size="sm" variant="outline" loading={list.loadingMore} onClick={list.loadMore}>Load more {noun}</Button> : null}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
package/blocks/intent/README.md
CHANGED
|
@@ -5,6 +5,20 @@ and score any account by strength and buying-committee coverage. The Apollo / 6s
|
|
|
5
5
|
|
|
6
6
|
Depends on: `crm` (account rows link to your company records). Install: `npx @graph8/sdk add intent`.
|
|
7
7
|
|
|
8
|
+
## Screens
|
|
9
|
+
|
|
10
|
+
`IntentScreen` (`components/intent-screen.tsx`) is the finished page: copy it as your route. No props are required.
|
|
11
|
+
|
|
12
|
+
- **Accounts tab**: the intent feed with a 7/30/90 day window, a high-intent filter, a keyword scope chip (`?kw=<keyword id>`),
|
|
13
|
+
a stat strip, and a detail panel for the clicked account (signals, trend, top keywords, buying-committee coverage). Deep link:
|
|
14
|
+
`?id=<company id>`; an account with no signal in the window shows "No intent for this account".
|
|
15
|
+
"Who visited this URL" lives under the table.
|
|
16
|
+
- **Keywords tab**: track keywords, preview then track a competitor domain, load the companies behind a keyword, stop tracking.
|
|
17
|
+
Clicking a keyword opens its detail; "Show accounts" there scopes the Accounts tab to it. Every billable or destructive
|
|
18
|
+
call is confirmed.
|
|
19
|
+
- Slots: `toolbarActions`, `renderDetailActions(account)`, `extraDetailTabs`.
|
|
20
|
+
- Prerequisites: ABM intent enabled for the workspace (403 is explained inline) and something to measure.
|
|
21
|
+
|
|
8
22
|
## Components
|
|
9
23
|
|
|
10
24
|
| Component | What it does | Tiers |
|
package/blocks/intent/block.json
CHANGED
|
@@ -61,6 +61,31 @@
|
|
|
61
61
|
"tiers": [
|
|
62
62
|
"read"
|
|
63
63
|
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "IntentScreen",
|
|
67
|
+
"file": "components/intent-screen.tsx",
|
|
68
|
+
"purpose": "Finished buyer-intent page: accounts feed with window and keyword filters, account detail panel (signals, top keywords, buying committee) and keyword/competitor tracking.",
|
|
69
|
+
"operations": [
|
|
70
|
+
"list_abm_companies_intent_abm_companies_get"
|
|
71
|
+
],
|
|
72
|
+
"tiers": [
|
|
73
|
+
"read"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "KeywordDetail",
|
|
78
|
+
"file": "components/keyword-detail.tsx",
|
|
79
|
+
"purpose": "Complete screen for one tracked keyword: fields, accounts showing intent, companies behind it, stop tracking.",
|
|
80
|
+
"operations": [
|
|
81
|
+
"keywords_list_intent_keywords_list_post",
|
|
82
|
+
"keyword_companies_intent_keywords__keyword_id__companies_post",
|
|
83
|
+
"keywords_delete_intent_keywords__keyword_id__delete"
|
|
84
|
+
],
|
|
85
|
+
"tiers": [
|
|
86
|
+
"billable",
|
|
87
|
+
"destructive"
|
|
88
|
+
]
|
|
64
89
|
}
|
|
65
90
|
],
|
|
66
91
|
"hooks": [
|
|
@@ -133,6 +158,7 @@
|
|
|
133
158
|
}
|
|
134
159
|
],
|
|
135
160
|
"peerDependencies": {
|
|
136
|
-
"react": ">=18"
|
|
161
|
+
"react": ">=18",
|
|
162
|
+
"lucide-react": ">=0.400"
|
|
137
163
|
}
|
|
138
164
|
}
|
|
@@ -36,7 +36,7 @@ function Coverage({ account }: { account: AbmAccount }) {
|
|
|
36
36
|
<li
|
|
37
37
|
key={category}
|
|
38
38
|
title={`${cov.contact_count ?? 0} contacts, ${cov.signal_count ?? 0} signals`}
|
|
39
|
-
className="rounded border border-
|
|
39
|
+
className="rounded border border-border px-1.5 py-0.5 text-xs text-foreground"
|
|
40
40
|
>
|
|
41
41
|
{category}: {cov.barometer ?? "none"}
|
|
42
42
|
</li>
|
|
@@ -52,9 +52,9 @@ function Badge({ account, compact }: { account: AbmAccount; compact?: boolean })
|
|
|
52
52
|
<div className="flex items-center gap-2">
|
|
53
53
|
<span className={`rounded px-2 py-0.5 text-xs font-medium ${strengthClass(account.account_strength)}`}>{label}</span>
|
|
54
54
|
{account.is_high_intent ? (
|
|
55
|
-
<span className="rounded bg-
|
|
55
|
+
<span className="rounded bg-error px-2 py-0.5 text-xs font-medium text-error-foreground">High intent</span>
|
|
56
56
|
) : null}
|
|
57
|
-
<span className="text-xs text-
|
|
57
|
+
<span className="text-xs text-muted-foreground">{account.total_signal_count ?? 0} signals</span>
|
|
58
58
|
</div>
|
|
59
59
|
{compact ? null : <Coverage account={account} />}
|
|
60
60
|
</div>
|
|
@@ -69,7 +69,7 @@ export function AccountIntentScore({ companyIds, days = 30, compact, className }
|
|
|
69
69
|
const status = (error as { status?: number }).status;
|
|
70
70
|
const text = status === 403 ? "Intent scoring is not enabled for this workspace." : "Could not load intent.";
|
|
71
71
|
return (
|
|
72
|
-
<span role="alert" className={`text-xs text-
|
|
72
|
+
<span role="alert" className={`text-xs text-error-foreground ${className ?? ""}`}>
|
|
73
73
|
{text}
|
|
74
74
|
</span>
|
|
75
75
|
);
|
|
@@ -81,7 +81,7 @@ export function AccountIntentScore({ companyIds, days = 30, compact, className }
|
|
|
81
81
|
return account ? (
|
|
82
82
|
<Badge key={id} account={{ ...account, company_id: id }} compact={compact} />
|
|
83
83
|
) : (
|
|
84
|
-
<span key={id} className="block text-xs text-
|
|
84
|
+
<span key={id} className="block text-xs text-muted-foreground">
|
|
85
85
|
No signal
|
|
86
86
|
</span>
|
|
87
87
|
);
|