@graph8/sdk 0.370.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +6 -0
- package/blocks/crm/block.json +53 -4
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +2 -2
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +4 -1
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +51 -17
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +36 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +113 -1
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +36 -8
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +8 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +20 -5
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +27 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
package/blocks/inbox/block.json
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"get_reply_inbox__reply_id__get",
|
|
22
22
|
"list_inbox_inbox_get",
|
|
23
23
|
"list_inbox_linkedin_accounts_inbox_linkedin_accounts_get",
|
|
24
|
+
"list_inbox_mailboxes_inbox_mailboxes_get",
|
|
24
25
|
"list_inbox_users_inbox_users_get",
|
|
25
26
|
"list_sms_phone_numbers_sms_phone_numbers_get",
|
|
27
|
+
"search_inbox_emails_inbox_emails_search_post",
|
|
26
28
|
"send_reply_inbox__reply_id__send_post",
|
|
27
29
|
"tag_reply_inbox__reply_id__tag_post"
|
|
28
30
|
],
|
|
@@ -94,6 +96,19 @@
|
|
|
94
96
|
"purpose": "Small status pill shared by the thread view and tag panel.",
|
|
95
97
|
"operations": [],
|
|
96
98
|
"tiers": []
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "RecordConversationsTab",
|
|
102
|
+
"file": "components/record-conversations-tab.tsx",
|
|
103
|
+
"purpose": "The email conversations with one contact (matched by address), with Load more and a calm answer for a thread that 404s; an extraDetailTabs entry for ContactsScreen.",
|
|
104
|
+
"operations": [
|
|
105
|
+
"list_inbox_mailboxes_inbox_mailboxes_get",
|
|
106
|
+
"search_inbox_emails_inbox_emails_search_post",
|
|
107
|
+
"get_reply_inbox__reply_id__get"
|
|
108
|
+
],
|
|
109
|
+
"tiers": [
|
|
110
|
+
"read"
|
|
111
|
+
]
|
|
97
112
|
}
|
|
98
113
|
],
|
|
99
114
|
"hooks": [
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordConversationsTab: the email conversations with one contact, newest first, "Load more" for older ones. Opening a row
|
|
5
|
+
* shows its messages. Use it as an `extraDetailTabs` entry of ContactsScreen:
|
|
6
|
+
* { id: "conversations", label: "Conversations", render: (c) => <RecordConversationsTab email={c.work_email} onOpenThread={(sel) => router.push(`/inbox?id=${sel}`)} /> }
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listInboxMailboxes GET /inbox/mailboxes (read; the mailboxes this key can read: search requires them)
|
|
10
|
+
* 2. searchInboxEmails POST /inbox/emails/search?page=&page_size= (read; body `{ mailboxes, search_emails: [email] }`, 10 per page)
|
|
11
|
+
* 3. getReply GET /inbox/{id}?channel=email (read; only when a row is opened)
|
|
12
|
+
* The inbox has no filter by contact id, so the contact is matched by email address (sender or recipient): `email` plus any
|
|
13
|
+
* `alsoEmails`. A contact with no usable address has no conversations to show. SMS and LinkedIn threads are not matched here.
|
|
14
|
+
*
|
|
15
|
+
* Hidden threads: the API answers a thread the key may not see exactly like one that does not exist (404, the same text).
|
|
16
|
+
* A row that 404s on open says "not available to you" instead of an error with a retry button.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { MessagesSquare } from "lucide-react";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
26
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { useInboxThread } from "../hooks/use-inbox-thread";
|
|
28
|
+
import { formatWhen, stripHtml, type InboxMessage } from "../lib/inbox-types";
|
|
29
|
+
|
|
30
|
+
const PAGE_SIZE = 10;
|
|
31
|
+
|
|
32
|
+
/** One thread of POST /inbox/emails/search. Only the fields shown here are read; all are optional. */
|
|
33
|
+
interface FoundThread { id?: string | number | null; email_id?: string | number | null; subject?: string | null; updated_at?: string | null; created_at?: string | null; status?: string | null; messages?: InboxMessage[] | null }
|
|
34
|
+
|
|
35
|
+
const idOf = (t: FoundThread): string => String(t.id ?? t.email_id ?? "");
|
|
36
|
+
const statusOf = (e: unknown): number | undefined => (e as { status?: number } | null)?.status;
|
|
37
|
+
|
|
38
|
+
/** Mailbox addresses from GET /inbox/mailboxes (rows are mailbox records; the address is `email`). */
|
|
39
|
+
function mailboxAddresses(env: unknown): string[] {
|
|
40
|
+
const data = (env as { data?: unknown } | null)?.data;
|
|
41
|
+
const rows = Array.isArray(data) ? data : ((data as { items?: unknown } | null)?.items ?? []);
|
|
42
|
+
const out = new Set<string>();
|
|
43
|
+
for (const r of Array.isArray(rows) ? rows : []) {
|
|
44
|
+
const row = r as { email?: string | null; mailbox?: string | null };
|
|
45
|
+
const address = (row.email ?? row.mailbox ?? "").trim();
|
|
46
|
+
if (address) out.add(address);
|
|
47
|
+
}
|
|
48
|
+
return [...out];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function Opened({ id }: { id: string }) {
|
|
52
|
+
const t = useInboxThread(id, "email");
|
|
53
|
+
if (t.error) {
|
|
54
|
+
return statusOf(t.error) === 404
|
|
55
|
+
? <p role="status" className="p-3 text-sm text-muted-foreground">This conversation is not available to you. It may have been removed, or it belongs to a mailbox you cannot read.</p>
|
|
56
|
+
: <div className="p-3"><ApiError error={t.error} title="Could not load this conversation" onRetry={() => void t.refetch()} /></div>;
|
|
57
|
+
}
|
|
58
|
+
if (!t.data) return <Skeleton className="m-3 h-16 w-auto" aria-label="Loading conversation" />;
|
|
59
|
+
const messages = t.data.messages.filter((m) => !m.is_draft);
|
|
60
|
+
if (messages.length === 0) return <p className="p-3 text-sm text-muted-foreground">No messages in this conversation yet.</p>;
|
|
61
|
+
return (
|
|
62
|
+
<ol className="space-y-2 p-3" aria-label="Messages">
|
|
63
|
+
{messages.map((m, i) => (
|
|
64
|
+
<li key={m.message_id ?? i} className="rounded-md border p-3 text-sm">
|
|
65
|
+
<p className="mb-1 flex flex-wrap gap-2 text-xs text-muted-foreground"><span className="font-medium text-foreground">{m.from_address ?? "Unknown sender"}</span>{m.date ? <span>{formatWhen(m.date)}</span> : null}</p>
|
|
66
|
+
<p className="whitespace-pre-wrap">{stripHtml(m.content) || "(no content)"}</p>
|
|
67
|
+
</li>
|
|
68
|
+
))}
|
|
69
|
+
</ol>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RecordConversationsTabProps {
|
|
74
|
+
/** The contact's email address. Without one the tab explains there is nothing to match. */
|
|
75
|
+
email: string | null | undefined;
|
|
76
|
+
/** Other addresses of the contact (personal or additional emails; a comma-separated string is fine). Threads with any of them are shown. */
|
|
77
|
+
alsoEmails?: ReadonlyArray<string | null | undefined>;
|
|
78
|
+
/** Route to the conversation in your inbox screen; receives the inbox selection ("email:<id>"). Without it the rows link to `${inboxPath}?id=email:<id>`. */
|
|
79
|
+
onOpenThread?: (selection: string) => void;
|
|
80
|
+
/** Path of your inbox screen for the plain links. Default "/inbox". */
|
|
81
|
+
inboxPath?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function RecordConversationsTab({ email, alsoEmails = [], onOpenThread, inboxPath = "/inbox" }: RecordConversationsTabProps) {
|
|
85
|
+
const addresses = [...new Set([email, ...alsoEmails].flatMap((e) => (e ?? "").split(",")).map((e) => e.trim()).filter((e) => e !== "" && !e.includes("*") && e.includes("@")))];
|
|
86
|
+
const usable = addresses.length > 0;
|
|
87
|
+
const boxes = useG8Query(async (signal) => mailboxAddresses(await g8Get("/inbox/mailboxes", undefined, { signal })), [usable], { enabled: usable });
|
|
88
|
+
const mailboxes = boxes.data ?? [];
|
|
89
|
+
|
|
90
|
+
if (!usable) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No email address" description="Conversations are matched by email address, and this contact has none to match." />;
|
|
91
|
+
if (boxes.error) return <ApiError error={boxes.error} title="Could not load mailboxes" onRetry={() => void boxes.refetch()} />;
|
|
92
|
+
if (!boxes.data) return <Skeleton className="h-24 w-full" aria-label="Loading conversations" />;
|
|
93
|
+
if (mailboxes.length === 0) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No mailbox connected" description="Connect a mailbox to see the conversations you have with this contact." />;
|
|
94
|
+
// Mounted only once the mailboxes are known, so its first render is already "loading" (no empty-state flash before the first search).
|
|
95
|
+
return <ConversationList addresses={addresses} mailboxes={mailboxes} onOpenThread={onOpenThread} inboxPath={inboxPath} />;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface ConversationListProps { addresses: string[]; mailboxes: string[]; onOpenThread?: (selection: string) => void; inboxPath: string }
|
|
99
|
+
|
|
100
|
+
function ConversationList({ addresses, mailboxes, onOpenThread, inboxPath }: ConversationListProps) {
|
|
101
|
+
const [open, setOpen] = useState<string | null>(null);
|
|
102
|
+
const list = usePaginated<FoundThread>(async (cursor, signal) => {
|
|
103
|
+
const page = cursor ? Number(cursor) || 1 : 1;
|
|
104
|
+
const env = await g8Post<{ items?: FoundThread[] }>(`/inbox/emails/search?page=${page}&page_size=${PAGE_SIZE}`, { mailboxes, search_emails: addresses }, { signal });
|
|
105
|
+
const items = (env.data?.items ?? []).filter((t) => idOf(t) !== "");
|
|
106
|
+
const more = (env as { pagination?: { has_next?: boolean } }).pagination?.has_next ?? items.length >= PAGE_SIZE;
|
|
107
|
+
return { data: items, pagination: { next_cursor: more && items.length > 0 ? String(page + 1) : null } } as never;
|
|
108
|
+
}, [addresses.join(","), mailboxes.join("|")]);
|
|
109
|
+
|
|
110
|
+
if (list.error) return <ApiError error={list.error} title="Could not load conversations" onRetry={() => void list.reload()} />;
|
|
111
|
+
if (list.loading && list.items.length === 0) return <Skeleton className="h-24 w-full" aria-label="Loading conversations" />;
|
|
112
|
+
if (list.items.length === 0) return <EmptyState compact icon={<MessagesSquare className="size-8" />} title="No conversations yet" description="Emails sent to or received from this contact show up here." />;
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="space-y-2">
|
|
116
|
+
<ul className="divide-y rounded-md border" aria-label="Conversations">
|
|
117
|
+
{list.items.map((t) => {
|
|
118
|
+
const id = idOf(t);
|
|
119
|
+
const last = (t.messages ?? []).filter((m) => !m.is_draft).slice(-1)[0];
|
|
120
|
+
const isOpen = open === id;
|
|
121
|
+
const selection = `email:${id}`;
|
|
122
|
+
return (
|
|
123
|
+
<li key={id}>
|
|
124
|
+
<div className="flex items-center justify-between gap-3 p-3 text-sm">
|
|
125
|
+
<button type="button" aria-expanded={isOpen} className="min-w-0 flex-1 text-left" onClick={() => setOpen(isOpen ? null : id)}>
|
|
126
|
+
<span className="block truncate font-medium">{t.subject || "(no subject)"}</span>
|
|
127
|
+
<span className="block truncate text-xs text-muted-foreground">{[formatWhen(t.updated_at ?? t.created_at), last ? stripHtml(last.content).slice(0, 80) : null].filter(Boolean).join(" · ")}</span>
|
|
128
|
+
</button>
|
|
129
|
+
<a
|
|
130
|
+
href={`${inboxPath}?id=${encodeURIComponent(selection)}`}
|
|
131
|
+
className="shrink-0 text-xs text-muted-foreground hover:underline"
|
|
132
|
+
onClick={(e) => {
|
|
133
|
+
if (!onOpenThread || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
onOpenThread(selection);
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
Open in inbox
|
|
139
|
+
</a>
|
|
140
|
+
</div>
|
|
141
|
+
{isOpen ? <Opened id={id} /> : null}
|
|
142
|
+
</li>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
</ul>
|
|
146
|
+
{list.hasMore ? (
|
|
147
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
148
|
+
<span role="status">Showing {list.items.length.toLocaleString()} conversations</span>
|
|
149
|
+
<Button size="sm" variant="outline" loading={list.loadingMore} onClick={() => void list.loadMore()}>Load more</Button>
|
|
150
|
+
</div>
|
|
151
|
+
) : null}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -8,8 +8,8 @@ import type { ReactNode } from "react";
|
|
|
8
8
|
|
|
9
9
|
const TONES = {
|
|
10
10
|
neutral: "bg-muted text-foreground",
|
|
11
|
-
info: "bg-
|
|
12
|
-
warn: "bg-
|
|
11
|
+
info: "bg-info text-info-foreground",
|
|
12
|
+
warn: "bg-warning text-warning-foreground",
|
|
13
13
|
} as const;
|
|
14
14
|
|
|
15
15
|
export function ThreadBadge({ children, tone = "neutral" }: { children: ReactNode; tone?: keyof typeof TONES }) {
|
|
@@ -50,7 +50,7 @@ function MessageBubble({ message, outbound }: { message: InboxMessage; outbound:
|
|
|
50
50
|
function ThreadHeader({ thread }: { thread: InboxThread }) {
|
|
51
51
|
return (
|
|
52
52
|
<header className="space-y-1 border-b pb-3">
|
|
53
|
-
<h2 className="text-base font-
|
|
53
|
+
<h2 className="text-base font-medium">{thread.subject || contactLabel(thread)}</h2>
|
|
54
54
|
<p className="text-sm text-muted-foreground">
|
|
55
55
|
{contactLabel(thread)}
|
|
56
56
|
{thread.contact?.company ? ` at ${thread.contact.company}` : ""} · <span className="capitalize">{thread.channel}</span>
|
|
@@ -11,7 +11,11 @@ Depends on: `foundation`. Install: `npx @graph8/sdk add integrations`.
|
|
|
11
11
|
|
|
12
12
|
- **Connections tab**: the five CRMs and the workspace tools. "Manage sync" opens a side panel with sync switches, job progress and a manual
|
|
13
13
|
push (deep link `?id=hubspot`; a webhook is `?tab=webhooks&id=<webhook id>`). Connecting is a link out to the graph8 app, because OAuth consent only runs there.
|
|
14
|
+
- A connected CRM's panel also has: **Health** (live status, connection record, CRM account, last historic import), **Sync now** (push or pull contacts, deals and leads as background jobs you can follow, with a lead push preview; each sync asks first), **CRM records** (browse contacts, companies, deals, leads and the activity log inside the CRM, read only), **Fields** (the CRM's fields beside graph8's) and **Historic import** (scan, review the counts, import, then sync later changes).
|
|
15
|
+
- **SugarCRM** is the one CRM the API can sign in to (instance address, user, password), so its panel has a **Connection** tab instead of a link-out; the other four link out to graph8.
|
|
14
16
|
- **Webhooks tab**: create, pause, rotate and delete webhooks, read deliveries. The secret is shown once.
|
|
17
|
+
- **Tools tab**: Slack, Google Workspace and Roam with live status and a "Connect in graph8" link for any that is not connected; Google also has a connection test and a Drive folder browser.
|
|
18
|
+
- **Ad accounts tab**: the ad platforms graph8 can sync audiences to, their accounts, a warning for any account the platform has paused, and a connect link-out.
|
|
15
19
|
- Props (all optional): `connectUrl`, `defaultWebhookUrl`, `pushRecords`, `toolbarActions`, `renderDetailActions(record)`, `extraDetailTabs` (each `{ id, label, render(record) }`), where `record` is `{ kind: "crm", provider }` or `{ kind: "webhook", id }`.
|
|
16
20
|
|
|
17
21
|
## Components
|
|
@@ -21,6 +25,14 @@ Depends on: `foundation`. Install: `npx @graph8/sdk add integrations`.
|
|
|
21
25
|
| `IntegrationList` | HubSpot, Salesforce, Pipedrive, Zoho and SugarCRM status, plus Slack, Google Workspace and Roam. Connect links to the app; Disconnect is confirmed. | read, destructive |
|
|
22
26
|
| `SyncStatus` | One CRM: health, enable/disable continuous push and pull, job progress, manual contact push. | read, external |
|
|
23
27
|
| `WebhookManager` | Create, pause, rotate and delete webhooks; secret shown once; delivery log; server-side verification note. | read, external, destructive |
|
|
28
|
+
| `ConnectionHealth` | One CRM connection: live status, record, account metadata, last historic import. | read |
|
|
29
|
+
| `SyncTriggers` | Run a sync now (contacts, deals, leads, push or pull), with a limit, a lead preview and job progress. | read, external |
|
|
30
|
+
| `CrmRecords` | Browse the records and activity log inside a connected CRM, read only. | read |
|
|
31
|
+
| `FieldMappings` | The CRM's fields next to the graph8 fields they can map to. | read |
|
|
32
|
+
| `MigrationPanel` | Scan a CRM, review, import its history, sync later changes. | read, external, destructive |
|
|
33
|
+
| `SugarCrmConnect` | Connect, check, update and remove a SugarCRM login. | read, external, destructive |
|
|
34
|
+
| `WorkspaceTools` | Slack, Google Workspace and Roam status, connect link-outs, Google test and Drive browser. | read |
|
|
35
|
+
| `AdAccounts` | Ad platforms, accounts and paused-account warnings. | read |
|
|
24
36
|
|
|
25
37
|
`external` and `destructive` actions open `ConfirmSpendDialog` first (they write to the customer's CRM,
|
|
26
38
|
change where data is sent, or remove a subscription).
|
|
@@ -50,7 +62,8 @@ reach the browser, which is why `WebhookManager` only displays it once at creati
|
|
|
50
62
|
|
|
51
63
|
- CRM connected via the app's OAuth screen (`where: "app"`). Verify: `GET /crm-syncs/{provider}/status` -> `connected: true`.
|
|
52
64
|
- A public https webhook route with `G8_WEBHOOK_SECRET` set (`where: "app"`).
|
|
53
|
-
- The API key's user has integrations/webhooks admin rights (`where: "api"`).
|
|
65
|
+
- The API key's user has integrations/webhooks admin rights (`where: "api"`). Migrations and the SugarCRM sign-in need an organisation admin.
|
|
66
|
+
- The proxy `allowedPrefixes` include `integrations`, `crm-syncs`, `webhooks` and `audience-syncs` (the ad platform reads).
|
|
54
67
|
|
|
55
68
|
## Defaults (do not ask the human)
|
|
56
69
|
|
|
@@ -80,5 +93,45 @@ reach the browser, which is why `WebhookManager` only displays it once at creati
|
|
|
80
93
|
| `rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post` | POST /webhooks/{webhook_id}/rotate-secret | external | WebhookManager |
|
|
81
94
|
| `list_webhook_deliveries_webhooks__webhook_id__deliveries_get` | GET /webhooks/{webhook_id}/deliveries | read | WebhookManager |
|
|
82
95
|
| `delete_webhook_webhooks__webhook_id__delete` | DELETE /webhooks/{webhook_id} | destructive | WebhookManager |
|
|
96
|
+
| `cancel_migration_integrations_migrations__migration_id__cancel_post` | POST /integrations/migrations/{migration_id}/cancel | destructive | MigrationPanel |
|
|
97
|
+
| `confirm_migration_integrations_migrations__migration_id__confirm_post` | POST /integrations/migrations/{migration_id}/confirm | external | MigrationPanel |
|
|
98
|
+
| `disconnect_sugarcrm_integrations_sugarcrm_delete` | DELETE /integrations/sugarcrm | destructive | SugarCrmConnect |
|
|
99
|
+
| `discover_crm_fields_integrations_crm_fields_get` | GET /integrations/crm/fields | read | FieldMappings |
|
|
100
|
+
| `get_crm_company_integrations_crm_connections__connection_id__companies__company_id__get` | GET /integrations/crm/connections/{connection_id}/companies/{company_id} | read | CrmRecords |
|
|
101
|
+
| `get_crm_connection_integrations_crm_connections__connection_id__get` | GET /integrations/crm/connections/{connection_id} | read | ConnectionHealth |
|
|
102
|
+
| `get_crm_connection_metadata_integrations_crm_connections__connection_id__metadata_get` | GET /integrations/crm/connections/{connection_id}/metadata | read | ConnectionHealth |
|
|
103
|
+
| `get_crm_connection_status_integrations_crm_connections__connection_id__status_get` | GET /integrations/crm/connections/{connection_id}/status | read | ConnectionHealth |
|
|
104
|
+
| `get_crm_contact_integrations_crm_connections__connection_id__contacts__contact_id__get` | GET /integrations/crm/connections/{connection_id}/contacts/{contact_id} | read | CrmRecords |
|
|
105
|
+
| `get_crm_deal_integrations_crm_connections__connection_id__deals__deal_id__get` | GET /integrations/crm/connections/{connection_id}/deals/{deal_id} | read | CrmRecords |
|
|
106
|
+
| `get_crm_lead_integrations_crm_connections__connection_id__leads__lead_id__get` | GET /integrations/crm/connections/{connection_id}/leads/{lead_id} | read | CrmRecords |
|
|
107
|
+
| `get_last_completed_migration_integrations_connections__connection_id__last_migration_get` | GET /integrations/connections/{connection_id}/last-migration | read | ConnectionHealth, MigrationPanel |
|
|
108
|
+
| `get_migration_status_integrations_migrations__migration_id__get` | GET /integrations/migrations/{migration_id} | read | MigrationPanel |
|
|
109
|
+
| `get_webhook_webhooks__webhook_id__get` | GET /webhooks/{webhook_id} | read | WebhookDetail |
|
|
110
|
+
| `list_ad_account_health_audience_syncs_account_health_get` | GET /audience-syncs/account-health | read | AdAccounts |
|
|
111
|
+
| `list_ad_accounts_audience_syncs_accounts_get` | GET /audience-syncs/accounts | read | AdAccounts |
|
|
112
|
+
| `list_ad_platforms_audience_syncs_platforms_get` | GET /audience-syncs/platforms | read | AdAccounts |
|
|
113
|
+
| `list_crm_activities_integrations_crm_activities_get` | GET /integrations/crm/activities | read | CrmRecords |
|
|
114
|
+
| `list_crm_companies_integrations_crm_connections__connection_id__companies_get` | GET /integrations/crm/connections/{connection_id}/companies | read | CrmRecords |
|
|
115
|
+
| `list_crm_contact_activities_integrations_crm_connections__connection_id__contacts__contact_id__activities_get` | GET /integrations/crm/connections/{connection_id}/contacts/{contact_id}/activities | read | CrmRecords |
|
|
116
|
+
| `list_crm_contacts_integrations_crm_connections__connection_id__contacts_get` | GET /integrations/crm/connections/{connection_id}/contacts | read | CrmRecords |
|
|
117
|
+
| `list_crm_deals_integrations_crm_connections__connection_id__deals_get` | GET /integrations/crm/connections/{connection_id}/deals | read | CrmRecords |
|
|
118
|
+
| `list_crm_leads_integrations_crm_connections__connection_id__leads_get` | GET /integrations/crm/connections/{connection_id}/leads | read | CrmRecords |
|
|
119
|
+
| `list_google_drive_folder_files_integrations_google_workspace_folders__folder_id__files_get` | GET /integrations/google-workspace/folders/{folder_id}/files | read | WorkspaceTools |
|
|
120
|
+
| `list_google_drive_folders_integrations_google_workspace_folders_get` | GET /integrations/google-workspace/folders | read | WorkspaceTools |
|
|
121
|
+
| `list_lead_statuses_integrations_crm_sync_leads_statuses_get` | GET /integrations/crm-sync/leads/statuses | read | SyncTriggers |
|
|
122
|
+
| `list_mappable_graph8_fields_integrations_crm_fields_mappable_get` | GET /integrations/crm/fields/mappable | read | FieldMappings |
|
|
123
|
+
| `preview_lead_push_integrations_crm_sync_leads_push_preview_get` | GET /integrations/crm-sync/leads/push-preview | read | SyncTriggers |
|
|
124
|
+
| `pull_all_contacts_integrations_crm_sync_contacts_pull_post` | POST /integrations/crm-sync/contacts/pull | external | SyncTriggers |
|
|
125
|
+
| `pull_all_deals_integrations_crm_sync_deals_pull_post` | POST /integrations/crm-sync/deals/pull | external | SyncTriggers |
|
|
126
|
+
| `pull_all_leads_integrations_crm_sync_leads_pull_post` | POST /integrations/crm-sync/leads/pull | external | SyncTriggers |
|
|
127
|
+
| `push_all_contacts_integrations_crm_sync_contacts_push_post` | POST /integrations/crm-sync/contacts/push | external | SyncTriggers |
|
|
128
|
+
| `push_all_deals_integrations_crm_sync_deals_push_post` | POST /integrations/crm-sync/deals/push | external | SyncTriggers |
|
|
129
|
+
| `push_all_leads_integrations_crm_sync_leads_push_post` | POST /integrations/crm-sync/leads/push | external | SyncTriggers |
|
|
130
|
+
| `start_delta_sync_integrations_migrations_delta_sync_post` | POST /integrations/migrations/delta-sync | external | MigrationPanel |
|
|
131
|
+
| `start_migration_discovery_integrations_migrations_discovery_post` | POST /integrations/migrations/discovery | external | MigrationPanel |
|
|
132
|
+
| `test_google_workspace_connection_integrations_google_workspace_test_post` | POST /integrations/google-workspace/test | read | WorkspaceTools |
|
|
133
|
+
| `validate_sugarcrm_integrations_sugarcrm_validate_post` | POST /integrations/sugarcrm/validate | read | SugarCrmConnect |
|
|
134
|
+
|
|
135
|
+
Not offered on purpose: CRM pass-through writes (create, update and delete a contact, company, deal, lead or meeting directly in the CRM, and the batch versions) because they bypass graph8's records and field mapping (they stay in recipes and `g8.api.integrations`); the embedded connection session token and `saveConnection` (the OAuth consent screen only runs in the graph8 app); syncing a graph8 list or audience (needs a list picker and starts from the Lists screen); Google Ads conversions and video assets (the Ads screen owns audience syncs).
|
|
83
136
|
|
|
84
137
|
Recipes (`recipes/`): `verify-webhook-route.ts`, `register-webhook.ts`, `crm-sync-health.ts`, `push-contacts-to-crm.ts`.
|
|
@@ -12,25 +12,63 @@
|
|
|
12
12
|
"foundation"
|
|
13
13
|
],
|
|
14
14
|
"operations": [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"get_google_workspace_status_integrations_google_workspace_get",
|
|
19
|
-
"get_roam_status_integrations_roam_status_get",
|
|
15
|
+
"cancel_migration_integrations_migrations__migration_id__cancel_post",
|
|
16
|
+
"confirm_migration_integrations_migrations__migration_id__confirm_post",
|
|
17
|
+
"create_webhook_webhooks_post",
|
|
20
18
|
"delete_connection_integrations_connections__connection_id__delete",
|
|
21
|
-
"
|
|
19
|
+
"delete_webhook_webhooks__webhook_id__delete",
|
|
20
|
+
"disconnect_sugarcrm_integrations_sugarcrm_delete",
|
|
21
|
+
"discover_crm_fields_integrations_crm_fields_get",
|
|
22
|
+
"get_crm_company_integrations_crm_connections__connection_id__companies__company_id__get",
|
|
23
|
+
"get_crm_connection_integrations_crm_connections__connection_id__get",
|
|
24
|
+
"get_crm_connection_metadata_integrations_crm_connections__connection_id__metadata_get",
|
|
25
|
+
"get_crm_connection_status_integrations_crm_connections__connection_id__status_get",
|
|
26
|
+
"get_crm_contact_integrations_crm_connections__connection_id__contacts__contact_id__get",
|
|
27
|
+
"get_crm_deal_integrations_crm_connections__connection_id__deals__deal_id__get",
|
|
22
28
|
"get_crm_fields_crm_syncs__provider__fields_get",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
29
|
+
"get_crm_lead_integrations_crm_connections__connection_id__leads__lead_id__get",
|
|
30
|
+
"get_crm_status_crm_syncs__provider__status_get",
|
|
31
|
+
"get_google_workspace_status_integrations_google_workspace_get",
|
|
32
|
+
"get_last_completed_migration_integrations_connections__connection_id__last_migration_get",
|
|
33
|
+
"get_migration_status_integrations_migrations__migration_id__get",
|
|
34
|
+
"get_roam_status_integrations_roam_status_get",
|
|
35
|
+
"get_slack_status_integrations_slack_status_get",
|
|
25
36
|
"get_sync_job_integrations_crm_sync_jobs__job_id__get",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
37
|
+
"get_webhook_webhooks__webhook_id__get",
|
|
38
|
+
"list_ad_account_health_audience_syncs_account_health_get",
|
|
39
|
+
"list_ad_accounts_audience_syncs_accounts_get",
|
|
40
|
+
"list_ad_platforms_audience_syncs_platforms_get",
|
|
41
|
+
"list_connections_integrations_connections_get",
|
|
42
|
+
"list_crm_activities_integrations_crm_activities_get",
|
|
43
|
+
"list_crm_companies_integrations_crm_connections__connection_id__companies_get",
|
|
44
|
+
"list_crm_contact_activities_integrations_crm_connections__connection_id__contacts__contact_id__activities_get",
|
|
45
|
+
"list_crm_contacts_integrations_crm_connections__connection_id__contacts_get",
|
|
46
|
+
"list_crm_deals_integrations_crm_connections__connection_id__deals_get",
|
|
47
|
+
"list_crm_leads_integrations_crm_connections__connection_id__leads_get",
|
|
48
|
+
"list_crm_syncs_crm_syncs_get",
|
|
49
|
+
"list_google_drive_folder_files_integrations_google_workspace_folders__folder_id__files_get",
|
|
50
|
+
"list_google_drive_folders_integrations_google_workspace_folders_get",
|
|
51
|
+
"list_lead_statuses_integrations_crm_sync_leads_statuses_get",
|
|
52
|
+
"list_mappable_graph8_fields_integrations_crm_fields_mappable_get",
|
|
53
|
+
"list_webhook_deliveries_webhooks__webhook_id__deliveries_get",
|
|
28
54
|
"list_webhook_events_webhooks_events_get",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
55
|
+
"list_webhooks_webhooks_get",
|
|
56
|
+
"preview_lead_push_integrations_crm_sync_leads_push_preview_get",
|
|
57
|
+
"pull_all_contacts_integrations_crm_sync_contacts_pull_post",
|
|
58
|
+
"pull_all_deals_integrations_crm_sync_deals_pull_post",
|
|
59
|
+
"pull_all_leads_integrations_crm_sync_leads_pull_post",
|
|
60
|
+
"push_all_contacts_integrations_crm_sync_contacts_push_post",
|
|
61
|
+
"push_all_deals_integrations_crm_sync_deals_push_post",
|
|
62
|
+
"push_all_leads_integrations_crm_sync_leads_push_post",
|
|
63
|
+
"push_contacts_to_crm_crm_syncs__provider__contacts_push_post",
|
|
31
64
|
"rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
65
|
+
"set_pull_sync_enabled_integrations_crm_sync_pull_enabled_put",
|
|
66
|
+
"set_push_sync_enabled_integrations_crm_sync_push_enabled_put",
|
|
67
|
+
"start_delta_sync_integrations_migrations_delta_sync_post",
|
|
68
|
+
"start_migration_discovery_integrations_migrations_discovery_post",
|
|
69
|
+
"test_google_workspace_connection_integrations_google_workspace_test_post",
|
|
70
|
+
"update_webhook_webhooks__webhook_id__patch",
|
|
71
|
+
"validate_sugarcrm_integrations_sugarcrm_validate_post"
|
|
34
72
|
],
|
|
35
73
|
"components": [
|
|
36
74
|
{
|
|
@@ -122,13 +160,137 @@
|
|
|
122
160
|
"list_webhook_deliveries_webhooks__webhook_id__deliveries_get",
|
|
123
161
|
"update_webhook_webhooks__webhook_id__patch",
|
|
124
162
|
"rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post",
|
|
125
|
-
"delete_webhook_webhooks__webhook_id__delete"
|
|
163
|
+
"delete_webhook_webhooks__webhook_id__delete",
|
|
164
|
+
"get_webhook_webhooks__webhook_id__get"
|
|
165
|
+
],
|
|
166
|
+
"tiers": [
|
|
167
|
+
"read",
|
|
168
|
+
"external",
|
|
169
|
+
"destructive"
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "ConnectionHealth",
|
|
174
|
+
"file": "components/connection-health.tsx",
|
|
175
|
+
"purpose": "Live status, connection record, CRM account details and the last historic import for one CRM connection.",
|
|
176
|
+
"operations": [
|
|
177
|
+
"get_crm_connection_integrations_crm_connections__connection_id__get",
|
|
178
|
+
"get_crm_connection_status_integrations_crm_connections__connection_id__status_get",
|
|
179
|
+
"get_crm_connection_metadata_integrations_crm_connections__connection_id__metadata_get",
|
|
180
|
+
"get_last_completed_migration_integrations_connections__connection_id__last_migration_get"
|
|
181
|
+
],
|
|
182
|
+
"tiers": [
|
|
183
|
+
"read"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"name": "SyncTriggers",
|
|
188
|
+
"file": "components/sync-triggers.tsx",
|
|
189
|
+
"purpose": "Run a sync now: push or pull contacts, deals and leads as background jobs with a lead push preview; every sync confirmed.",
|
|
190
|
+
"operations": [
|
|
191
|
+
"get_sync_job_integrations_crm_sync_jobs__job_id__get",
|
|
192
|
+
"list_lead_statuses_integrations_crm_sync_leads_statuses_get",
|
|
193
|
+
"preview_lead_push_integrations_crm_sync_leads_push_preview_get",
|
|
194
|
+
"push_all_contacts_integrations_crm_sync_contacts_push_post",
|
|
195
|
+
"pull_all_contacts_integrations_crm_sync_contacts_pull_post",
|
|
196
|
+
"push_all_deals_integrations_crm_sync_deals_push_post",
|
|
197
|
+
"pull_all_deals_integrations_crm_sync_deals_pull_post",
|
|
198
|
+
"push_all_leads_integrations_crm_sync_leads_push_post",
|
|
199
|
+
"pull_all_leads_integrations_crm_sync_leads_pull_post"
|
|
200
|
+
],
|
|
201
|
+
"tiers": [
|
|
202
|
+
"read",
|
|
203
|
+
"external"
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "CrmRecords",
|
|
208
|
+
"file": "components/crm-records.tsx",
|
|
209
|
+
"purpose": "Browse the contacts, companies, deals, leads and activity log inside a connected CRM, read only.",
|
|
210
|
+
"operations": [
|
|
211
|
+
"list_crm_contact_activities_integrations_crm_connections__connection_id__contacts__contact_id__activities_get",
|
|
212
|
+
"list_crm_activities_integrations_crm_activities_get",
|
|
213
|
+
"list_crm_contacts_integrations_crm_connections__connection_id__contacts_get",
|
|
214
|
+
"list_crm_companies_integrations_crm_connections__connection_id__companies_get",
|
|
215
|
+
"list_crm_deals_integrations_crm_connections__connection_id__deals_get",
|
|
216
|
+
"list_crm_leads_integrations_crm_connections__connection_id__leads_get",
|
|
217
|
+
"get_crm_contact_integrations_crm_connections__connection_id__contacts__contact_id__get",
|
|
218
|
+
"get_crm_company_integrations_crm_connections__connection_id__companies__company_id__get",
|
|
219
|
+
"get_crm_deal_integrations_crm_connections__connection_id__deals__deal_id__get",
|
|
220
|
+
"get_crm_lead_integrations_crm_connections__connection_id__leads__lead_id__get"
|
|
221
|
+
],
|
|
222
|
+
"tiers": [
|
|
223
|
+
"read"
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "FieldMappings",
|
|
228
|
+
"file": "components/field-mappings.tsx",
|
|
229
|
+
"purpose": "The CRM's fields next to the graph8 fields they can map to, searchable, read only.",
|
|
230
|
+
"operations": [
|
|
231
|
+
"discover_crm_fields_integrations_crm_fields_get",
|
|
232
|
+
"list_mappable_graph8_fields_integrations_crm_fields_mappable_get"
|
|
233
|
+
],
|
|
234
|
+
"tiers": [
|
|
235
|
+
"read"
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "MigrationPanel",
|
|
240
|
+
"file": "components/migration-panel.tsx",
|
|
241
|
+
"purpose": "Scan a CRM, review the counts, import its history, sync later changes; every step confirmed.",
|
|
242
|
+
"operations": [
|
|
243
|
+
"get_last_completed_migration_integrations_connections__connection_id__last_migration_get",
|
|
244
|
+
"get_migration_status_integrations_migrations__migration_id__get",
|
|
245
|
+
"start_migration_discovery_integrations_migrations_discovery_post",
|
|
246
|
+
"start_delta_sync_integrations_migrations_delta_sync_post",
|
|
247
|
+
"confirm_migration_integrations_migrations__migration_id__confirm_post",
|
|
248
|
+
"cancel_migration_integrations_migrations__migration_id__cancel_post"
|
|
126
249
|
],
|
|
127
250
|
"tiers": [
|
|
128
251
|
"read",
|
|
129
252
|
"external",
|
|
130
253
|
"destructive"
|
|
131
254
|
]
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"name": "SugarCrmConnect",
|
|
258
|
+
"file": "components/sugarcrm-connect.tsx",
|
|
259
|
+
"purpose": "Connect SugarCRM with an instance address and login (the one CRM the API can sign in to), check, update credentials, remove.",
|
|
260
|
+
"operations": [
|
|
261
|
+
"validate_sugarcrm_integrations_sugarcrm_validate_post",
|
|
262
|
+
"disconnect_sugarcrm_integrations_sugarcrm_delete"
|
|
263
|
+
],
|
|
264
|
+
"tiers": [
|
|
265
|
+
"read",
|
|
266
|
+
"destructive"
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "WorkspaceTools",
|
|
271
|
+
"file": "components/workspace-tools.tsx",
|
|
272
|
+
"purpose": "Slack, Google Workspace and Roam with live status and a connect link-out; Google connection test and Drive browser.",
|
|
273
|
+
"operations": [
|
|
274
|
+
"list_google_drive_folders_integrations_google_workspace_folders_get",
|
|
275
|
+
"list_google_drive_folder_files_integrations_google_workspace_folders__folder_id__files_get",
|
|
276
|
+
"test_google_workspace_connection_integrations_google_workspace_test_post"
|
|
277
|
+
],
|
|
278
|
+
"tiers": [
|
|
279
|
+
"read"
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"name": "AdAccounts",
|
|
284
|
+
"file": "components/ad-accounts.tsx",
|
|
285
|
+
"purpose": "Ad platforms, their accounts and any account the platform has paused, with a connect link-out.",
|
|
286
|
+
"operations": [
|
|
287
|
+
"list_ad_platforms_audience_syncs_platforms_get",
|
|
288
|
+
"list_ad_accounts_audience_syncs_accounts_get",
|
|
289
|
+
"list_ad_account_health_audience_syncs_account_health_get"
|
|
290
|
+
],
|
|
291
|
+
"tiers": [
|
|
292
|
+
"read"
|
|
293
|
+
]
|
|
132
294
|
}
|
|
133
295
|
],
|
|
134
296
|
"hooks": [],
|
|
@@ -136,6 +298,18 @@
|
|
|
136
298
|
{
|
|
137
299
|
"file": "lib/integrations-utils.ts",
|
|
138
300
|
"purpose": "CRM provider list and labels, status types and envelope helpers shared by the integrations components."
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"file": "lib/load-more.tsx",
|
|
304
|
+
"purpose": "Paging helpers (useLoadMoreList, useGrowingList, LoadMore) so no list stops silently at a cap."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"file": "lib/crm-utils.ts",
|
|
308
|
+
"purpose": "Readers for records in a CRM's own shape: title, subtitle, flattened fields."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"file": "lib/facts.tsx",
|
|
312
|
+
"purpose": "FactList: renders an open-shaped API object as labelled facts."
|
|
139
313
|
}
|
|
140
314
|
],
|
|
141
315
|
"recipes": [
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AdAccounts: the ad platforms graph8 can sync audiences to (Google Ads, Meta, LinkedIn and others), whether each is connected, how many syncs
|
|
5
|
+
* it carries, which accounts are available, and a warning for any account the platform has paused or de-authorised (the usual reason a
|
|
6
|
+
* sync stopped). A platform that is not connected shows a "Connect in graph8" link: the sign-in screen only runs in the graph8 app.
|
|
7
|
+
* All reads; the health list is the last hourly check, not a live call. Creating and running the syncs themselves is on the Ads screen.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listAdPlatforms GET /audience-syncs/platforms (read) platform, connected, active_syncs
|
|
11
|
+
* 2. listAdAccounts GET /audience-syncs/accounts (read) the accounts you can sync to
|
|
12
|
+
* 3. listAdAccountHealth GET /audience-syncs/account-health (read) only the accounts that are NOT healthy
|
|
13
|
+
*/
|
|
14
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
17
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
18
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
21
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
22
|
+
import { arrayOf, GRAPH8_APP_INTEGRATIONS_PATH } from "../lib/integrations-utils";
|
|
23
|
+
|
|
24
|
+
export interface AdAccountsProps {
|
|
25
|
+
/** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
|
|
26
|
+
connectUrl?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Platform { platform: string; connected?: boolean; account_name?: string | null; account_id?: string | null; active_syncs?: number }
|
|
31
|
+
// /audience-syncs/accounts answers ad_account_id / ad_account_name.
|
|
32
|
+
interface Account { platform?: string; ad_account_id?: string | number; ad_account_name?: string }
|
|
33
|
+
interface Health { platform?: string; account_id?: string | number; health?: string; reason?: string; message?: string; detail?: string }
|
|
34
|
+
const label = (s: string): string => s.replace(/[_-]/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
35
|
+
|
|
36
|
+
export function AdAccounts({ connectUrl: connectUrlProp, className }: AdAccountsProps) {
|
|
37
|
+
const brand = useBrand();
|
|
38
|
+
const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
|
|
39
|
+
const platforms = useG8Query((signal) => g8Get("/audience-syncs/platforms", undefined, { signal }), []);
|
|
40
|
+
const accounts = useG8Query((signal) => g8Get("/audience-syncs/accounts", undefined, { signal }), []);
|
|
41
|
+
const health = useG8Query((signal) => g8Get("/audience-syncs/account-health", undefined, { signal }), []);
|
|
42
|
+
const unhealthy = arrayOf<Health>(health.data);
|
|
43
|
+
const accountRows = arrayOf<Account>(accounts.data);
|
|
44
|
+
const platformRows = arrayOf<Platform>(platforms.data);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
48
|
+
{unhealthy.length > 0 ? (
|
|
49
|
+
<div role="alert" className="rounded-md border border-error-foreground/25 bg-error p-3 text-sm text-error-foreground" data-testid="ad-health-warning">
|
|
50
|
+
<p className="font-medium">{unhealthy.length === 1 ? "An ad account needs attention" : `${unhealthy.length} ad accounts need attention`}</p>
|
|
51
|
+
<ul className="mt-1 list-disc pl-5">{unhealthy.map((h, i) => <li key={`${h.platform}-${h.account_id}-${i}`}>{label(h.platform ?? "Platform")} account {String(h.account_id ?? "")}: {label(h.health ?? "unhealthy")}{h.reason || h.message || h.detail ? ` (${h.reason ?? h.message ?? h.detail})` : ""}</li>)}</ul>
|
|
52
|
+
</div>
|
|
53
|
+
) : null}
|
|
54
|
+
{health.error ? <ApiError compact error={health.error} title="Could not check account health" onRetry={() => void health.refetch()} /> : null}
|
|
55
|
+
<SectionCard title="Ad platforms" description={`Where ${brand.name} can sync audiences`} flush>
|
|
56
|
+
{platforms.error ? <div className="p-3"><ApiError error={platforms.error} title="Could not load the platforms" onRetry={() => void platforms.refetch()} /></div> : null}
|
|
57
|
+
{platforms.loading && !platforms.data ? <Skeleton className="m-3 h-12" /> : null}
|
|
58
|
+
<ul className="divide-y" aria-label="Ad platforms">
|
|
59
|
+
{platformRows.map((p) => (
|
|
60
|
+
<li key={p.platform} data-platform={p.platform} className="flex flex-wrap items-center justify-between gap-2 px-4 py-3 text-sm">
|
|
61
|
+
<span><span className="font-medium">{label(p.platform)}</span><span className="block text-xs text-muted-foreground">{p.connected ? `${p.active_syncs ?? 0} active ${p.active_syncs === 1 ? "sync" : "syncs"}${p.account_name ? `, ${p.account_name}` : ""}` : "Not connected"}</span></span>
|
|
62
|
+
{p.connected ? <StatusPill tone="positive" dot>Connected</StatusPill> : <a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel}</a>}
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
{platforms.data && platformRows.length === 0 ? <EmptyState compact title="No ad platforms" description="Ad platforms appear here once they are available to your workspace." /> : null}
|
|
67
|
+
</SectionCard>
|
|
68
|
+
<SectionCard title="Accounts" description="The ad accounts you can sync to" flush>
|
|
69
|
+
{accounts.error ? <div className="p-3"><ApiError error={accounts.error} title="Could not load the accounts" onRetry={() => void accounts.refetch()} /></div> : null}
|
|
70
|
+
{accounts.loading && !accounts.data ? <Skeleton className="m-3 h-12" /> : null}
|
|
71
|
+
<ul className="divide-y" aria-label="Ad accounts">
|
|
72
|
+
{accountRows.map((a, i) => {
|
|
73
|
+
const id = String(a.ad_account_id ?? "");
|
|
74
|
+
const bad = unhealthy.find((h) => String(h.account_id) === id && (!h.platform || !a.platform || h.platform === a.platform));
|
|
75
|
+
return <li key={`${a.platform}-${id}-${i}`} className="flex items-center justify-between gap-2 px-4 py-2 text-sm"><span>{a.ad_account_name || id}<span className="ml-2 text-xs text-muted-foreground">{a.platform ? label(a.platform) : ""} {id}</span></span><StatusPill tone={bad ? "negative" : "positive"} dot>{bad ? label(bad.health ?? "unhealthy") : "Healthy"}</StatusPill></li>;
|
|
76
|
+
})}
|
|
77
|
+
</ul>
|
|
78
|
+
{accounts.data && accountRows.length === 0 ? <EmptyState compact title="No ad accounts" description={`Connect an ad platform in ${brand.linkLabel} to see its accounts here.`} /> : null}
|
|
79
|
+
</SectionCard>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|