@graph8/sdk 0.370.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* SDK operations, in order:
|
|
9
9
|
* 1. listWebhooks GET /webhooks (read)
|
|
10
|
+
* getWebhook GET /webhooks/{id} (read; only when the id is not in the list, e.g. a deep link to a webhook beyond the list's page)
|
|
10
11
|
* 2. listWebhookEvents GET /webhooks/events (read; the event names to pick from)
|
|
11
12
|
* 3. listWebhookDeliveries GET /webhooks/{id}/deliveries (read)
|
|
12
13
|
* 4. updateWebhook PATCH /webhooks/{id} (external, confirmed; edit, pause, resume)
|
|
@@ -17,6 +18,9 @@
|
|
|
17
18
|
import { useEffect, useState, type ReactNode } from "react";
|
|
18
19
|
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
19
20
|
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
22
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
23
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
20
24
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
25
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
26
|
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
@@ -45,13 +49,24 @@ interface Hook { id: string; url: string; name?: string | null; events?: string[
|
|
|
45
49
|
interface EventType { event: string; category?: string | null; description?: string | null }
|
|
46
50
|
interface Delivery { id: string; event: string; status: string; attempts?: number; max_attempts?: number; response_code?: number | null; error_message?: string | null; created_at?: string | null; completed_at?: string | null }
|
|
47
51
|
|
|
52
|
+
/** Deliveries per read; "Load more" appends the next page. */
|
|
53
|
+
const DELIVERY_PAGE = 20;
|
|
54
|
+
|
|
48
55
|
export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WebhookDetailProps) {
|
|
49
56
|
const brand = useBrand();
|
|
50
57
|
const { ask, dialog, failure } = useConfirmAction();
|
|
51
58
|
const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
|
|
52
59
|
const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
|
|
53
|
-
const deliveries =
|
|
54
|
-
|
|
60
|
+
const deliveries = useLoadMoreList(byPage<Delivery>(DELIVERY_PAGE, async (page, signal) => {
|
|
61
|
+
const env = await g8Get(`/webhooks/${webhookId}/deliveries`, { page, limit: DELIVERY_PAGE }, { signal });
|
|
62
|
+
const rows = arrayOf<Delivery>(env);
|
|
63
|
+
return { rows, hasNext: hasNextOf(env, DELIVERY_PAGE, rows.length) };
|
|
64
|
+
}), [webhookId]);
|
|
65
|
+
const listed = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
|
|
66
|
+
const single = useG8Query((signal) => g8Get<Hook>(`/webhooks/${webhookId}`, undefined, { signal }), [webhookId], { enabled: Boolean(hooks.data) && !listed });
|
|
67
|
+
const fetched = (single.data as { data?: Hook } | undefined)?.data;
|
|
68
|
+
// Only the record fetched for THIS id counts: useG8Query keeps the previous webhook's data while the next one loads.
|
|
69
|
+
const hook = listed ?? (fetched?.id === webhookId ? fetched : undefined);
|
|
55
70
|
const [form, setForm] = useState({ url: "", name: "", events: [] as string[] });
|
|
56
71
|
const [secret, setSecret] = useState<string | null>(null);
|
|
57
72
|
|
|
@@ -60,14 +75,15 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
|
|
|
60
75
|
}, [hook?.id, hook?.url, hook?.name, hook?.events?.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
61
76
|
|
|
62
77
|
if (hooks.error) return <ApiError error={hooks.error} title="Could not load the webhook" onRetry={() => void hooks.refetch()} />;
|
|
63
|
-
if (!
|
|
78
|
+
if (!hook && single.error) return <ApiError error={single.error} title="Could not load the webhook" onRetry={() => void single.refetch()} />;
|
|
79
|
+
if (!hooks.data || (!hook && single.loading)) return <Skeleton className="h-40 w-full" />;
|
|
64
80
|
if (!hook) return <EmptyState title="Webhook not found" description="It may have been deleted." />;
|
|
65
|
-
const changed = () => { void hooks.refetch(); onChanged?.(); };
|
|
81
|
+
const changed = () => { void hooks.refetch(); if (!listed) void single.refetch(); onChanged?.(); };
|
|
66
82
|
const active = hook.is_active !== false;
|
|
67
83
|
const byCategory = arrayOf<EventType>(events.data).reduce<Record<string, EventType[]>>((acc, e) => ({ ...acc, [e.category ?? "other"]: [...(acc[e.category ?? "other"] ?? []), e] }), {});
|
|
68
84
|
const toggle = (event: string) => setForm((f) => ({ ...f, events: f.events.includes(event) ? f.events.filter((x) => x !== event) : [...f.events, event] }));
|
|
69
85
|
const urlOk = /^https:\/\/\S+$/.test(form.url.trim());
|
|
70
|
-
const rows =
|
|
86
|
+
const rows = deliveries.data ?? [];
|
|
71
87
|
|
|
72
88
|
const actions = (
|
|
73
89
|
<div className="flex flex-wrap gap-1">
|
|
@@ -88,9 +104,9 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
|
|
|
88
104
|
content: (
|
|
89
105
|
<div className="space-y-4">
|
|
90
106
|
{secret ? (
|
|
91
|
-
<div role="alert" className="rounded border border-
|
|
107
|
+
<div role="alert" className="rounded border border-warning-foreground/30 bg-warning p-3 text-sm">
|
|
92
108
|
<p className="font-medium">New signing secret. It is shown once.</p>
|
|
93
|
-
<code className="mt-1 block break-all rounded bg-
|
|
109
|
+
<code className="mt-1 block break-all rounded bg-background p-2">{secret}</code>
|
|
94
110
|
<p className="mt-1 text-xs">Store it as G8_WEBHOOK_SECRET on your server. Never put it in browser code.</p>
|
|
95
111
|
<Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>I saved it</Button>
|
|
96
112
|
</div>
|
|
@@ -137,6 +153,7 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
|
|
|
137
153
|
id: "deliveries",
|
|
138
154
|
label: "Deliveries",
|
|
139
155
|
content: deliveries.loading && !deliveries.data ? <Skeleton className="h-16 w-full" /> : deliveries.error ? <ApiError error={deliveries.error} onRetry={() => void deliveries.refetch()} /> : rows.length === 0 ? <p className="text-sm text-muted-foreground">No deliveries yet.</p> : (
|
|
156
|
+
<>
|
|
140
157
|
<table className="w-full text-sm" aria-label="Deliveries">
|
|
141
158
|
<thead><tr className="text-left text-muted-foreground"><th>Event</th><th>Status</th><th>Code</th><th>Attempts</th><th>When</th></tr></thead>
|
|
142
159
|
<tbody>
|
|
@@ -147,6 +164,8 @@ export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChan
|
|
|
147
164
|
))}
|
|
148
165
|
</tbody>
|
|
149
166
|
</table>
|
|
167
|
+
<LoadMore shown={rows.length} noun="deliveries" hasMore={deliveries.hasMore} loading={deliveries.loadingMore} onLoadMore={deliveries.loadMore} />
|
|
168
|
+
</>
|
|
150
169
|
),
|
|
151
170
|
},
|
|
152
171
|
];
|
|
@@ -23,6 +23,9 @@ 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";
|
|
@@ -105,16 +108,24 @@ function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier
|
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
/** Deliveries per read; "Load more" appends the next page. */
|
|
112
|
+
const DELIVERY_PAGE = 20;
|
|
113
|
+
|
|
108
114
|
function Deliveries({ hook }: { hook: Webhook }) {
|
|
109
|
-
const list =
|
|
110
|
-
|
|
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" />;
|
|
111
121
|
if (list.error) return <ApiError error={list.error} onRetry={() => void list.refetch()} />;
|
|
112
|
-
const rows =
|
|
113
|
-
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>;
|
|
114
124
|
return (
|
|
125
|
+
<>
|
|
115
126
|
<table className="w-full text-sm" aria-label="Deliveries">
|
|
116
127
|
<thead>
|
|
117
|
-
<tr className="text-left text-
|
|
128
|
+
<tr className="text-left text-muted-foreground">
|
|
118
129
|
<th>Event</th>
|
|
119
130
|
<th>Status</th>
|
|
120
131
|
<th>Code</th>
|
|
@@ -134,6 +145,8 @@ function Deliveries({ hook }: { hook: Webhook }) {
|
|
|
134
145
|
))}
|
|
135
146
|
</tbody>
|
|
136
147
|
</table>
|
|
148
|
+
<LoadMore shown={rows.length} noun="deliveries" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
149
|
+
</>
|
|
137
150
|
);
|
|
138
151
|
}
|
|
139
152
|
|
|
@@ -178,8 +191,8 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
|
|
|
178
191
|
|
|
179
192
|
return (
|
|
180
193
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
181
|
-
<section className="space-y-3 rounded border border-
|
|
182
|
-
<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>
|
|
183
196
|
<div className="grid gap-2 md:grid-cols-2">
|
|
184
197
|
<Input
|
|
185
198
|
aria-label="Endpoint URL"
|
|
@@ -189,14 +202,14 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
|
|
|
189
202
|
/>
|
|
190
203
|
<Input aria-label="Name" placeholder="Name (optional)" value={name} onChange={(e) => setName(e.target.value)} />
|
|
191
204
|
</div>
|
|
192
|
-
{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}
|
|
193
206
|
{events.loading ? (
|
|
194
207
|
<Skeleton className="h-12 w-full" />
|
|
195
208
|
) : (
|
|
196
209
|
<div className="space-y-2">
|
|
197
210
|
{Object.entries(byCategory).map(([category, items]) => (
|
|
198
211
|
<fieldset key={category}>
|
|
199
|
-
<legend className="text-xs font-medium uppercase text-
|
|
212
|
+
<legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
|
|
200
213
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
201
214
|
{items.map((e) => (
|
|
202
215
|
<label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}>
|
|
@@ -218,10 +231,10 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
|
|
|
218
231
|
</section>
|
|
219
232
|
|
|
220
233
|
{secret ? (
|
|
221
|
-
<div role="alert" className="rounded border border-
|
|
234
|
+
<div role="alert" className="rounded border border-warning-foreground/30 bg-warning p-3 text-sm">
|
|
222
235
|
<p className="font-medium">Signing secret for {secret.url}. It is shown once.</p>
|
|
223
|
-
<code className="mt-1 block break-all rounded bg-
|
|
224
|
-
<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">
|
|
225
238
|
Store it in your server environment as G8_WEBHOOK_SECRET. Never put it in browser code.
|
|
226
239
|
</p>
|
|
227
240
|
<Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>
|
|
@@ -238,13 +251,13 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
|
|
|
238
251
|
) : list.length === 0 ? (
|
|
239
252
|
<EmptyState title="No webhooks yet" description="Create one above to receive events like replies and meetings on your server." />
|
|
240
253
|
) : (
|
|
241
|
-
<ul className="divide-y rounded border border-
|
|
254
|
+
<ul className="divide-y rounded border border-border">
|
|
242
255
|
{list.map((h) => (
|
|
243
256
|
<li key={h.id} className="space-y-2 px-3 py-3">
|
|
244
257
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
245
258
|
<div>
|
|
246
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>}
|
|
247
|
-
<div className="text-xs text-
|
|
260
|
+
<div className="text-xs text-muted-foreground">
|
|
248
261
|
{h.name ? `${h.url} - ` : ""}
|
|
249
262
|
{(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}
|
|
250
263
|
</div>
|
|
@@ -275,12 +288,12 @@ export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }:
|
|
|
275
288
|
</ul>
|
|
276
289
|
)}
|
|
277
290
|
|
|
278
|
-
<details className="rounded border border-
|
|
291
|
+
<details className="rounded border border-border p-3 text-sm">
|
|
279
292
|
<summary className="cursor-pointer font-medium">Verify deliveries (server side)</summary>
|
|
280
|
-
<p className="mt-2 text-
|
|
293
|
+
<p className="mt-2 text-muted-foreground">
|
|
281
294
|
Check the signature on the raw body before trusting an event. Deliveries repeat, so deduplicate on the event id.
|
|
282
295
|
</p>
|
|
283
|
-
<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">
|
|
284
297
|
<code>{`const event = g8.webhooks.constructEvent(rawBody,
|
|
285
298
|
req.headers.get("x-studio-signature") ?? "",
|
|
286
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
|
+
}
|
|
@@ -7,6 +7,11 @@ import { DEFAULT_BRAND } from "@/g8/hooks/use-brand";
|
|
|
7
7
|
export const CRM_PROVIDERS = ["hubspot", "salesforce", "pipedrive", "zoho", "sugarcrm"] as const;
|
|
8
8
|
export type CrmProvider = (typeof CRM_PROVIDERS)[number];
|
|
9
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
|
+
|
|
10
15
|
export const PROVIDER_LABELS: Record<CrmProvider, string> = {
|
|
11
16
|
hubspot: "HubSpot",
|
|
12
17
|
salesforce: "Salesforce",
|
|
@@ -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
|
+
}
|
|
@@ -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
|
);
|