@graph8/sdk 0.361.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -85,6 +85,50 @@
|
|
|
85
85
|
"external",
|
|
86
86
|
"destructive"
|
|
87
87
|
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "IntegrationsScreen",
|
|
91
|
+
"file": "components/integrations-screen.tsx",
|
|
92
|
+
"purpose": "Finished integrations page: CRM and tool connections with a sync side panel, plus webhook management.",
|
|
93
|
+
"operations": [
|
|
94
|
+
"list_crm_syncs_crm_syncs_get",
|
|
95
|
+
"list_webhooks_webhooks_get"
|
|
96
|
+
],
|
|
97
|
+
"tiers": [
|
|
98
|
+
"read"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "CrmDetail",
|
|
103
|
+
"file": "components/crm-detail.tsx",
|
|
104
|
+
"purpose": "Complete screen for one CRM integration: connection, sync switches, manual push, disconnect.",
|
|
105
|
+
"operations": [
|
|
106
|
+
"list_crm_syncs_crm_syncs_get",
|
|
107
|
+
"list_connections_integrations_connections_get",
|
|
108
|
+
"delete_connection_integrations_connections__connection_id__delete"
|
|
109
|
+
],
|
|
110
|
+
"tiers": [
|
|
111
|
+
"read",
|
|
112
|
+
"destructive"
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "WebhookDetail",
|
|
117
|
+
"file": "components/webhook-detail.tsx",
|
|
118
|
+
"purpose": "Complete screen for one webhook: every field, edit, deliveries, pause, rotate secret, delete.",
|
|
119
|
+
"operations": [
|
|
120
|
+
"list_webhooks_webhooks_get",
|
|
121
|
+
"list_webhook_events_webhooks_events_get",
|
|
122
|
+
"list_webhook_deliveries_webhooks__webhook_id__deliveries_get",
|
|
123
|
+
"update_webhook_webhooks__webhook_id__patch",
|
|
124
|
+
"rotate_webhook_secret_webhooks__webhook_id__rotate_secret_post",
|
|
125
|
+
"delete_webhook_webhooks__webhook_id__delete"
|
|
126
|
+
],
|
|
127
|
+
"tiers": [
|
|
128
|
+
"read",
|
|
129
|
+
"external",
|
|
130
|
+
"destructive"
|
|
131
|
+
]
|
|
88
132
|
}
|
|
89
133
|
],
|
|
90
134
|
"hooks": [],
|
|
@@ -167,6 +211,7 @@
|
|
|
167
211
|
}
|
|
168
212
|
],
|
|
169
213
|
"peerDependencies": {
|
|
170
|
-
"react": ">=18"
|
|
214
|
+
"react": ">=18",
|
|
215
|
+
"lucide-react": ">=0.400"
|
|
171
216
|
}
|
|
172
217
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CrmDetail: the complete screen for one CRM integration (HubSpot, Salesforce, Pipedrive, Zoho, SugarCRM). Shows its connection
|
|
5
|
+
* (status, connected and last sync time, connection id), the continuous sync switches, job progress, manual push and contact fields
|
|
6
|
+
* (SyncStatus), and the actions: disconnect (destructive) when connected, or a link to connect in graph8 when it is not.
|
|
7
|
+
* Connecting is a link out: the OAuth consent screen only runs in the graph8 app.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listCrmSyncs GET /crm-syncs (read)
|
|
11
|
+
* 2. listConnections GET /integrations/connections (read; the connection to disconnect)
|
|
12
|
+
* 3. deleteConnection DELETE /integrations/connections/{id}?provider_config_key= (destructive, confirmed)
|
|
13
|
+
* 4. SyncStatus see sync-status.tsx (getCrmStatus, getCrmFields, set push/pull enabled, getSyncJob, pushContactsToCrm)
|
|
14
|
+
*/
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
21
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
22
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
23
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { arrayOf, formatWhen, GRAPH8_APP_INTEGRATIONS_PATH, PROVIDER_LABELS } from "../lib/integrations-utils";
|
|
25
|
+
import type { ConnectionRow, CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
|
|
28
|
+
import { SyncStatus } from "./sync-status";
|
|
29
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
30
|
+
|
|
31
|
+
export interface CrmDetailProps {
|
|
32
|
+
provider: CrmProvider;
|
|
33
|
+
/** Where "Connect in graph8" sends the person. */
|
|
34
|
+
connectUrl?: string;
|
|
35
|
+
/** Contact records (in the CRM's field names) offered for a manual push. */
|
|
36
|
+
records?: ReadonlyArray<Record<string, unknown>>;
|
|
37
|
+
onClose?: () => void;
|
|
38
|
+
onExpand?: () => void;
|
|
39
|
+
onCollapse?: () => void;
|
|
40
|
+
onChanged?: () => void;
|
|
41
|
+
/** Extra header actions from the screen's `renderDetailActions` slot. */
|
|
42
|
+
extraActions?: ReactNode;
|
|
43
|
+
/** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
|
|
44
|
+
extraTabs?: { id: string; label: string; content: ReactNode }[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CONNECTED = ["connected", "active", "ok", "healthy", "syncing", "synced", "enabled", "linked"];
|
|
48
|
+
|
|
49
|
+
export function CrmDetail({ provider, connectUrl: connectUrlProp, records, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: CrmDetailProps) {
|
|
50
|
+
const brand = useBrand();
|
|
51
|
+
const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
|
|
52
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
53
|
+
const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
|
|
54
|
+
const connections = useG8Query((signal) => g8Get("/integrations/connections", undefined, { signal }), []);
|
|
55
|
+
const label = PROVIDER_LABELS[provider];
|
|
56
|
+
|
|
57
|
+
if (syncs.error) return <ApiError error={syncs.error} title="Could not load the integration" onRetry={() => void syncs.refetch()} />;
|
|
58
|
+
if (!syncs.data) return <Skeleton className="h-40 w-full" />;
|
|
59
|
+
const row = arrayOf<CrmSyncRow>(syncs.data).find((r) => r.provider?.toLowerCase() === provider);
|
|
60
|
+
const connected = Boolean(row && CONNECTED.includes(row.status?.toLowerCase()));
|
|
61
|
+
const all = arrayOf<ConnectionRow>(connections.data).filter((c) => (c.provider ?? "").toLowerCase() === provider || (c.provider_config_key ?? "").toLowerCase().split(/[-_]/)[0] === provider);
|
|
62
|
+
const connection = all.find((c) => row?.connection_id && c.connection_id === row.connection_id) ?? (all.length === 1 ? all[0] : undefined);
|
|
63
|
+
const changed = () => { void syncs.refetch(); void connections.refetch(); onChanged?.(); };
|
|
64
|
+
|
|
65
|
+
const actions = connected ? (
|
|
66
|
+
<AskButton ask={ask} label="Disconnect" variant="ghost" tier="destructive" title={`Disconnect ${label}`} confirmLabel="Disconnect"
|
|
67
|
+
disabled={!connection?.connection_id || !connection.provider_config_key}
|
|
68
|
+
description="Syncs configured against this connection stop working. Records already synced stay."
|
|
69
|
+
run={() => g8Delete(`/integrations/connections/${connection!.connection_id}`, { query: { provider_config_key: connection!.provider_config_key } })} onDone={changed} />
|
|
70
|
+
) : (
|
|
71
|
+
<a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel}</a>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const tabs = [
|
|
75
|
+
{
|
|
76
|
+
id: "overview",
|
|
77
|
+
label: "Overview",
|
|
78
|
+
content: (
|
|
79
|
+
<KeyValueList columns={2} items={[
|
|
80
|
+
{ label: "Provider", value: label },
|
|
81
|
+
{ label: "Status", value: row?.status ?? "not connected" },
|
|
82
|
+
{ label: "Connected", value: formatWhen(row?.connected_at) },
|
|
83
|
+
{ label: "Last sync", value: formatWhen(row?.last_sync_at) },
|
|
84
|
+
{ label: "Connection", value: row?.connection_id ?? connection?.connection_id ?? "-" },
|
|
85
|
+
{ label: "Config key", value: connection?.provider_config_key ?? "-" },
|
|
86
|
+
]} />
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
{ id: "sync", label: "Sync", content: <SyncStatus provider={provider} records={records} /> },
|
|
90
|
+
];
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<DetailPanel title={label} subtitle="CRM integration" avatar={<EntityAvatar name={label} kind="company" size="lg" />}
|
|
94
|
+
badges={<StatusPill tone={connected ? "positive" : "neutral"}>{connected ? "Connected" : row?.status ?? "Not connected"}</StatusPill>}
|
|
95
|
+
actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
|
|
96
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
97
|
+
<ConfirmSpendDialog {...dialog} />
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -26,17 +26,20 @@ import {
|
|
|
26
26
|
arrayOf,
|
|
27
27
|
CRM_PROVIDERS,
|
|
28
28
|
formatWhen,
|
|
29
|
-
|
|
29
|
+
GRAPH8_APP_INTEGRATIONS_PATH,
|
|
30
30
|
looksConnected,
|
|
31
31
|
PROVIDER_LABELS,
|
|
32
32
|
} from "../lib/integrations-utils";
|
|
33
33
|
import type { ConnectionRow, CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
|
|
34
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
34
35
|
|
|
35
36
|
export interface IntegrationListProps {
|
|
36
37
|
/** Where "Connect" sends the person. Default: graph8 Settings > Integrations. */
|
|
37
38
|
connectUrl?: string;
|
|
38
39
|
/** Called when the person opens a CRM's sync panel (render SyncStatus for it). */
|
|
39
40
|
onOpenSync?: (provider: CrmProvider) => void;
|
|
41
|
+
/** Called when a person opens any CRM, connected or not (show a CrmDetail). */
|
|
42
|
+
onSelectCrm?: (provider: CrmProvider) => void;
|
|
40
43
|
className?: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -78,7 +81,9 @@ function ChatTools() {
|
|
|
78
81
|
);
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
export function IntegrationList({ connectUrl
|
|
84
|
+
export function IntegrationList({ connectUrl: connectUrlProp, onOpenSync, onSelectCrm, className }: IntegrationListProps) {
|
|
85
|
+
const brand = useBrand();
|
|
86
|
+
const connectUrl = connectUrlProp ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
|
|
82
87
|
const [disconnecting, setDisconnecting] = useState<{ provider: CrmProvider; connection: ConnectionRow } | null>(null);
|
|
83
88
|
const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
|
|
84
89
|
const connections = useG8Query((signal) => g8Get("/integrations/connections", undefined, { signal }), []);
|
|
@@ -113,7 +118,7 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
|
|
|
113
118
|
return (
|
|
114
119
|
<li key={p} className="flex flex-wrap items-center justify-between gap-2 px-3 py-3" data-provider={p}>
|
|
115
120
|
<div>
|
|
116
|
-
<div className="font-medium">{PROVIDER_LABELS[p]}</div>
|
|
121
|
+
{onSelectCrm ? <button type="button" className="font-medium hover:underline" onClick={() => onSelectCrm(p)}>{PROVIDER_LABELS[p]}</button> : <div className="font-medium">{PROVIDER_LABELS[p]}</div>}
|
|
117
122
|
<div className="text-xs text-gray-500">
|
|
118
123
|
{connected ? `Last sync: ${formatWhen(row?.last_sync_at)}` : "Connect to sync contacts and companies."}
|
|
119
124
|
</div>
|
|
@@ -137,7 +142,7 @@ export function IntegrationList({ connectUrl = GRAPH8_APP_INTEGRATIONS_URL, onOp
|
|
|
137
142
|
) : (
|
|
138
143
|
<Button size="sm" asChild>
|
|
139
144
|
<a href={connectUrl} target="_blank" rel="noreferrer">
|
|
140
|
-
Connect in
|
|
145
|
+
Connect in {brand.linkLabel}
|
|
141
146
|
</a>
|
|
142
147
|
</Button>
|
|
143
148
|
)}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IntegrationsScreen: the finished integrations settings page. Two tabs:
|
|
5
|
+
* Connections the five CRMs and the workspace tools with their status (IntegrationList). Every CRM, connected or not, opens its
|
|
6
|
+
* complete detail screen (CrmDetail: connection, push/pull switches, job progress, manual push, disconnect; `?id=hubspot`).
|
|
7
|
+
* "Connect in graph8" links out: the OAuth consent screen only runs in the graph8 app.
|
|
8
|
+
* Webhooks create webhooks (WebhookManager); each opens its complete detail screen (WebhookDetail: every field, edit, deliveries,
|
|
9
|
+
* pause, rotate secret, delete; `?id=`). Every detail also opens as a full page with `?view=full`.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations: two reads for the stat strip (GET /crm-syncs, GET /webhooks). Composes IntegrationList (read +
|
|
12
|
+
* destructive, confirmed), SyncStatus (external, confirmed) and WebhookManager (external + destructive, confirmed).
|
|
13
|
+
* Prerequisites: a CRM connected in the app (where: "app"); a public https route with G8_WEBHOOK_SECRET for webhooks.
|
|
14
|
+
*/
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Plug } from "lucide-react";
|
|
18
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
21
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
22
|
+
import type { StatItem } from "@/g8/components/screen/stat-strip";
|
|
23
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { arrayOf, CRM_PROVIDERS, GRAPH8_APP_INTEGRATIONS_PATH, PROVIDER_LABELS } from "../lib/integrations-utils";
|
|
26
|
+
import type { CrmProvider, CrmSyncRow } from "../lib/integrations-utils";
|
|
27
|
+
import { IntegrationList } from "./integration-list";
|
|
28
|
+
import { CrmDetail } from "./crm-detail";
|
|
29
|
+
import { WebhookDetail } from "./webhook-detail";
|
|
30
|
+
import { WebhookManager } from "./webhook-manager";
|
|
31
|
+
import { brandUrl, useBrand } from "@/g8/hooks/use-brand";
|
|
32
|
+
|
|
33
|
+
export interface IntegrationsScreenProps {
|
|
34
|
+
/** Where "Connect" sends people. Default: graph8 Settings > Integrations. */
|
|
35
|
+
connectUrl?: string;
|
|
36
|
+
/** Pre-fill the webhook endpoint URL (your /api/webhooks/graph8 route). */
|
|
37
|
+
defaultWebhookUrl?: string;
|
|
38
|
+
/** Contact records (in the CRM's field names) offered for a manual push in the sync panel. */
|
|
39
|
+
pushRecords?: ReadonlyArray<Record<string, unknown>>;
|
|
40
|
+
toolbarActions?: ReactNode;
|
|
41
|
+
/** Buttons added to the open record's header: a CRM connection or a webhook. */
|
|
42
|
+
renderDetailActions?: (record: IntegrationRecord) => ReactNode;
|
|
43
|
+
/** Tabs added after the built-in ones on the open record. */
|
|
44
|
+
extraDetailTabs?: { id: string; label: string; render: (record: IntegrationRecord) => ReactNode }[];
|
|
45
|
+
className?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The record open in the detail panel, passed to the slot props. */
|
|
49
|
+
export type IntegrationRecord = { kind: "crm"; provider: CrmProvider } | { kind: "webhook"; id: string };
|
|
50
|
+
|
|
51
|
+
const CONNECTED = ["connected", "active", "ok", "healthy", "syncing", "synced", "enabled", "linked"];
|
|
52
|
+
|
|
53
|
+
function useStats(): StatItem[] | undefined {
|
|
54
|
+
const syncs = useG8Query((signal) => g8Get("/crm-syncs", undefined, { signal }), []);
|
|
55
|
+
const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
|
|
56
|
+
if (syncs.error) return undefined;
|
|
57
|
+
const rows = arrayOf<CrmSyncRow>(syncs.data);
|
|
58
|
+
const connected = rows.filter((r) => CONNECTED.includes(r.status?.toLowerCase())).length;
|
|
59
|
+
const items: StatItem[] = [
|
|
60
|
+
{ label: "CRMs connected", value: `${connected} of ${CRM_PROVIDERS.length}`, tone: connected ? "positive" : "neutral" },
|
|
61
|
+
];
|
|
62
|
+
if (!hooks.error && hooks.data) items.push({ label: "Webhooks", value: arrayOf<unknown>(hooks.data).length });
|
|
63
|
+
return syncs.data ? items : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function IntegrationsScreen(props: IntegrationsScreenProps) {
|
|
67
|
+
const brand = useBrand();
|
|
68
|
+
const connectUrl = props.connectUrl ?? brandUrl(brand, GRAPH8_APP_INTEGRATIONS_PATH);
|
|
69
|
+
const [tabValue, setTab] = useUrlState("tab", "connections");
|
|
70
|
+
const tab = tabValue === "webhooks" ? "webhooks" : "connections";
|
|
71
|
+
const [id, setId] = useUrlState("id", "");
|
|
72
|
+
const [view, setView] = useUrlState("view", "");
|
|
73
|
+
const expanded = view === "full" && Boolean(id);
|
|
74
|
+
const [tick, setTick] = useState(0);
|
|
75
|
+
const stats = useStats();
|
|
76
|
+
const close = () => {
|
|
77
|
+
setId("");
|
|
78
|
+
setView("");
|
|
79
|
+
};
|
|
80
|
+
const panel = {
|
|
81
|
+
onClose: close,
|
|
82
|
+
onExpand: expanded ? undefined : () => setView("full"),
|
|
83
|
+
onCollapse: expanded ? () => setView("") : undefined,
|
|
84
|
+
onChanged: () => setTick((t) => t + 1),
|
|
85
|
+
};
|
|
86
|
+
const provider = (CRM_PROVIDERS as readonly string[]).includes(id) ? (id as CrmProvider) : null;
|
|
87
|
+
const slots = (record: IntegrationRecord) => ({
|
|
88
|
+
extraActions: props.renderDetailActions?.(record),
|
|
89
|
+
extraTabs: (props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(record) })),
|
|
90
|
+
});
|
|
91
|
+
const split = (list: ReactNode, detail: ReactNode, open: boolean) => (
|
|
92
|
+
<SplitView detailOpen={open} onCloseDetail={close} expanded={expanded} onExpandedChange={(next) => setView(next ? "full" : "")} detailWidth="lg" list={list} detail={open ? detail : null} />
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ScreenLayout
|
|
97
|
+
title="Integrations"
|
|
98
|
+
description={`Connect your CRM, keep it in sync, and send ${brand.linkLabel} events to your own systems.`}
|
|
99
|
+
icon={<Plug className="size-5" />}
|
|
100
|
+
actions={props.toolbarActions}
|
|
101
|
+
tabs={[
|
|
102
|
+
{ id: "connections", label: "Connections" },
|
|
103
|
+
{ id: "webhooks", label: "Webhooks" },
|
|
104
|
+
]}
|
|
105
|
+
activeTab={tab}
|
|
106
|
+
onTabChange={(next) => {
|
|
107
|
+
setTab(next);
|
|
108
|
+
close();
|
|
109
|
+
}}
|
|
110
|
+
stats={stats}
|
|
111
|
+
className={props.className}
|
|
112
|
+
>
|
|
113
|
+
{tab === "connections" ? (
|
|
114
|
+
<div className="space-y-4">
|
|
115
|
+
<SectionCard title="Connecting an account" description={`Sign-in with your CRM happens in ${brand.linkLabel}. Connect there, then manage the sync here.`}>
|
|
116
|
+
<a className="text-sm underline" href={connectUrl} target="_blank" rel="noreferrer">
|
|
117
|
+
Connect in {brand.linkLabel}
|
|
118
|
+
</a>
|
|
119
|
+
</SectionCard>
|
|
120
|
+
{split(
|
|
121
|
+
<IntegrationList key={tick} connectUrl={connectUrl} onSelectCrm={(p) => setId(p)} onOpenSync={(p) => setId(p)} />,
|
|
122
|
+
provider ? <CrmDetail key={provider} provider={provider} connectUrl={connectUrl} records={props.pushRecords} {...panel} {...slots({ kind: "crm", provider })} /> : null,
|
|
123
|
+
provider != null,
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
split(
|
|
128
|
+
<WebhookManager key={tick} defaultUrl={props.defaultWebhookUrl} onSelectWebhook={(h) => setId(h.id)} />,
|
|
129
|
+
<WebhookDetail key={id} webhookId={id} {...panel} {...slots({ kind: "webhook", id })} />,
|
|
130
|
+
Boolean(id),
|
|
131
|
+
)
|
|
132
|
+
)}
|
|
133
|
+
</ScreenLayout>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -26,6 +26,7 @@ import { ApiError } from "@/g8/components/api-error";
|
|
|
26
26
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
27
|
import { formatWhen, PROVIDER_LABELS, TERMINAL_JOB_STATES } from "../lib/integrations-utils";
|
|
28
28
|
import type { CrmProvider, CrmStatus } from "../lib/integrations-utils";
|
|
29
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
29
30
|
|
|
30
31
|
export interface SyncStatusProps {
|
|
31
32
|
provider: CrmProvider;
|
|
@@ -59,6 +60,7 @@ function JobProgress({ jobId }: { jobId: string }) {
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
export function SyncStatus({ provider, records, className }: SyncStatusProps) {
|
|
63
|
+
const brand = useBrand();
|
|
62
64
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
63
65
|
const [jobId, setJobId] = useState<string | null>(null);
|
|
64
66
|
const [pushed, setPushed] = useState<string | null>(null);
|
|
@@ -116,7 +118,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
|
|
|
116
118
|
if (!s?.connected) {
|
|
117
119
|
return (
|
|
118
120
|
<p role="status" className={`rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900 ${className ?? ""}`}>
|
|
119
|
-
{label} is not connected. {s?.message ??
|
|
121
|
+
{label} is not connected. {s?.message ?? `Connect it in ${brand.linkLabel}, then come back.`}
|
|
120
122
|
</p>
|
|
121
123
|
);
|
|
122
124
|
}
|
|
@@ -132,7 +134,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
|
|
|
132
134
|
? {
|
|
133
135
|
title: `${pending.enabled ? "Turn on" : "Turn off"} continuous ${pending.direction} with ${label}`,
|
|
134
136
|
description: pending.enabled
|
|
135
|
-
?
|
|
137
|
+
? `${brand.linkLabel} will keep ${pending.direction === "push" ? "writing contacts to" : "reading contacts from"} your CRM.`
|
|
136
138
|
: "Syncing in this direction stops.",
|
|
137
139
|
label: pending.enabled ? "Turn on" : "Turn off",
|
|
138
140
|
}
|
|
@@ -152,7 +154,7 @@ export function SyncStatus({ provider, records, className }: SyncStatusProps) {
|
|
|
152
154
|
|
|
153
155
|
{(["push", "pull"] as const).map((direction) => (
|
|
154
156
|
<div key={direction} className="flex items-center justify-between rounded border border-gray-200 px-3 py-2 text-sm">
|
|
155
|
-
<span>{direction === "push" ?
|
|
157
|
+
<span>{direction === "push" ? `Send ${brand.linkLabel} contacts to the CRM` : `Bring CRM contacts into ${brand.linkLabel}`}</span>
|
|
156
158
|
<span className="flex gap-2">
|
|
157
159
|
<Button
|
|
158
160
|
size="sm"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebhookDetail: the complete screen for one webhook. Finds it by id (so `?id=` deep links work), shows every field (endpoint, name,
|
|
5
|
+
* events, active state, field filters, created and updated times), its delivery log, and every action: rename, change the endpoint or
|
|
6
|
+
* the events, pause or resume, rotate the signing secret (shown once), delete.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listWebhooks GET /webhooks (read)
|
|
10
|
+
* 2. listWebhookEvents GET /webhooks/events (read; the event names to pick from)
|
|
11
|
+
* 3. listWebhookDeliveries GET /webhooks/{id}/deliveries (read)
|
|
12
|
+
* 4. updateWebhook PATCH /webhooks/{id} (external, confirmed; edit, pause, resume)
|
|
13
|
+
* 5. rotateWebhookSecret POST /webhooks/{id}/rotate-secret (external, confirmed; new secret shown once)
|
|
14
|
+
* 6. deleteWebhook DELETE /webhooks/{id} (destructive, confirmed; 404 counts as done)
|
|
15
|
+
* Verify deliveries on the server with g8.webhooks.constructEvent; the secret never belongs in browser code.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
18
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
23
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
24
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
25
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
26
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { arrayOf, formatWhen } from "../lib/integrations-utils";
|
|
28
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
29
|
+
import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
|
|
30
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
|
+
|
|
32
|
+
export interface WebhookDetailProps {
|
|
33
|
+
webhookId: string;
|
|
34
|
+
onClose?: () => void;
|
|
35
|
+
onExpand?: () => void;
|
|
36
|
+
onCollapse?: () => void;
|
|
37
|
+
onChanged?: () => void;
|
|
38
|
+
/** Extra header actions from the screen's `renderDetailActions` slot. */
|
|
39
|
+
extraActions?: ReactNode;
|
|
40
|
+
/** Extra tabs from the screen's `extraDetailTabs` slot, after the built-in ones. */
|
|
41
|
+
extraTabs?: { id: string; label: string; content: ReactNode }[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Hook { id: string; url: string; name?: string | null; events?: string[]; is_active?: boolean; created_at?: string | null; updated_at?: string | null; field_filters?: Record<string, string[]> | null; secret?: string | null }
|
|
45
|
+
interface EventType { event: string; category?: string | null; description?: string | null }
|
|
46
|
+
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
|
+
|
|
48
|
+
export function WebhookDetail({ webhookId, onClose, onExpand, onCollapse, onChanged, extraActions, extraTabs }: WebhookDetailProps) {
|
|
49
|
+
const brand = useBrand();
|
|
50
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
51
|
+
const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
|
|
52
|
+
const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
|
|
53
|
+
const deliveries = useG8Query((signal) => g8Get(`/webhooks/${webhookId}/deliveries`, { page: 1, limit: 20 }, { signal }), [webhookId]);
|
|
54
|
+
const hook = arrayOf<Hook>(hooks.data).find((h) => h.id === webhookId);
|
|
55
|
+
const [form, setForm] = useState({ url: "", name: "", events: [] as string[] });
|
|
56
|
+
const [secret, setSecret] = useState<string | null>(null);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (hook) setForm({ url: hook.url, name: hook.name ?? "", events: hook.events ?? [] });
|
|
60
|
+
}, [hook?.id, hook?.url, hook?.name, hook?.events?.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
61
|
+
|
|
62
|
+
if (hooks.error) return <ApiError error={hooks.error} title="Could not load the webhook" onRetry={() => void hooks.refetch()} />;
|
|
63
|
+
if (!hooks.data) return <Skeleton className="h-40 w-full" />;
|
|
64
|
+
if (!hook) return <EmptyState title="Webhook not found" description="It may have been deleted." />;
|
|
65
|
+
const changed = () => { void hooks.refetch(); onChanged?.(); };
|
|
66
|
+
const active = hook.is_active !== false;
|
|
67
|
+
const byCategory = arrayOf<EventType>(events.data).reduce<Record<string, EventType[]>>((acc, e) => ({ ...acc, [e.category ?? "other"]: [...(acc[e.category ?? "other"] ?? []), e] }), {});
|
|
68
|
+
const toggle = (event: string) => setForm((f) => ({ ...f, events: f.events.includes(event) ? f.events.filter((x) => x !== event) : [...f.events, event] }));
|
|
69
|
+
const urlOk = /^https:\/\/\S+$/.test(form.url.trim());
|
|
70
|
+
const rows = arrayOf<Delivery>(deliveries.data);
|
|
71
|
+
|
|
72
|
+
const actions = (
|
|
73
|
+
<div className="flex flex-wrap gap-1">
|
|
74
|
+
<AskButton ask={ask} label={active ? "Pause" : "Resume"} tier="external" title={`${active ? "Pause" : "Resume"} deliveries to ${hook.url}`} confirmLabel={active ? "Pause" : "Resume"}
|
|
75
|
+
description={active ? "Events stop being sent to this endpoint until you resume." : "Events are sent to this endpoint again."} run={() => g8Patch(`/webhooks/${hook.id}`, { is_active: !active })} onDone={changed} />
|
|
76
|
+
<AskButton ask={ask} label="Rotate secret" tier="external" title={`Rotate the signing secret for ${hook.url}`} confirmLabel="Rotate"
|
|
77
|
+
description="The old secret stops verifying deliveries as soon as you rotate. Update your server before or right after."
|
|
78
|
+
run={async () => { const res = await g8Post<Hook>(`/webhooks/${hook.id}/rotate-secret`); if (res.data?.secret) setSecret(res.data.secret); }} onDone={changed} />
|
|
79
|
+
<AskButton ask={ask} label="Delete" variant="ghost" tier="destructive" title={`Delete the webhook for ${hook.url}`} confirmLabel="Delete" tolerateStatus={[404]}
|
|
80
|
+
description={`${brand.linkLabel} stops sending events to this endpoint and removes its delivery log.`} run={() => g8Delete(`/webhooks/${hook.id}`)} onDone={() => { changed(); onClose?.(); }} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const tabs = [
|
|
85
|
+
{
|
|
86
|
+
id: "overview",
|
|
87
|
+
label: "Overview",
|
|
88
|
+
content: (
|
|
89
|
+
<div className="space-y-4">
|
|
90
|
+
{secret ? (
|
|
91
|
+
<div role="alert" className="rounded border border-amber-300 bg-amber-50 p-3 text-sm">
|
|
92
|
+
<p className="font-medium">New signing secret. It is shown once.</p>
|
|
93
|
+
<code className="mt-1 block break-all rounded bg-white p-2">{secret}</code>
|
|
94
|
+
<p className="mt-1 text-xs">Store it as G8_WEBHOOK_SECRET on your server. Never put it in browser code.</p>
|
|
95
|
+
<Button size="sm" variant="outline" className="mt-2" onClick={() => setSecret(null)}>I saved it</Button>
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
<KeyValueList columns={2} items={[
|
|
99
|
+
{ label: "Endpoint", value: hook.url },
|
|
100
|
+
{ label: "Name", value: hook.name ?? "-" },
|
|
101
|
+
{ label: "Status", value: active ? "active" : "paused" },
|
|
102
|
+
{ label: "Created", value: formatWhen(hook.created_at) },
|
|
103
|
+
{ label: "Updated", value: formatWhen(hook.updated_at) },
|
|
104
|
+
{ label: "Events", value: (hook.events ?? []).length },
|
|
105
|
+
]} />
|
|
106
|
+
<SectionCard title="Subscribed events"><ul className="flex flex-wrap gap-1 text-xs">{(hook.events ?? []).map((e) => <li key={e}><StatusPill tone="neutral">{e}</StatusPill></li>)}</ul></SectionCard>
|
|
107
|
+
{hook.field_filters && Object.keys(hook.field_filters).length > 0 ? (
|
|
108
|
+
<SectionCard title="Field filters" description="crm.record.updated fires only when a watched field changed."><pre className="overflow-x-auto text-xs">{JSON.stringify(hook.field_filters, null, 2)}</pre></SectionCard>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "edit",
|
|
115
|
+
label: "Edit",
|
|
116
|
+
content: (
|
|
117
|
+
<SectionCard title="Edit webhook" description="Changing where events go is confirmed first.">
|
|
118
|
+
<div className="space-y-3">
|
|
119
|
+
<div className="flex flex-col gap-1"><Label htmlFor="wd-url">Endpoint URL</Label><Input id="wd-url" value={form.url} onChange={(e) => setForm({ ...form, url: e.target.value })} /></div>
|
|
120
|
+
{form.url && !urlOk ? <p className="text-xs text-error-foreground">The endpoint must be an https:// URL.</p> : null}
|
|
121
|
+
<div className="flex flex-col gap-1"><Label htmlFor="wd-name">Name</Label><Input id="wd-name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></div>
|
|
122
|
+
{Object.entries(byCategory).map(([category, items]) => (
|
|
123
|
+
<fieldset key={category}>
|
|
124
|
+
<legend className="text-xs font-medium uppercase text-muted-foreground">{category}</legend>
|
|
125
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
126
|
+
{items.map((e) => <label key={e.event} className="flex items-center gap-1 text-sm" title={e.description ?? undefined}><input type="checkbox" checked={form.events.includes(e.event)} onChange={() => toggle(e.event)} />{e.event}</label>)}
|
|
127
|
+
</div>
|
|
128
|
+
</fieldset>
|
|
129
|
+
))}
|
|
130
|
+
<AskButton ask={ask} label="Save changes" variant="default" tier="external" title={`Update the webhook for ${hook.url}`} confirmLabel="Save" disabled={!urlOk || form.events.length === 0}
|
|
131
|
+
description={`Changes where ${brand.linkLabel} sends your data.`} run={() => g8Patch(`/webhooks/${hook.id}`, { url: form.url.trim(), name: form.name.trim() || null, events: form.events })} onDone={changed} />
|
|
132
|
+
</div>
|
|
133
|
+
</SectionCard>
|
|
134
|
+
),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "deliveries",
|
|
138
|
+
label: "Deliveries",
|
|
139
|
+
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> : (
|
|
140
|
+
<table className="w-full text-sm" aria-label="Deliveries">
|
|
141
|
+
<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
|
+
<tbody>
|
|
143
|
+
{rows.map((d) => (
|
|
144
|
+
<tr key={d.id} className="border-t" title={d.error_message ?? undefined}>
|
|
145
|
+
<td>{d.event}</td><td>{d.status}</td><td>{d.response_code ?? "-"}</td><td>{d.attempts ?? 1}{d.max_attempts ? ` of ${d.max_attempts}` : ""}</td><td>{formatWhen(d.created_at)}</td>
|
|
146
|
+
</tr>
|
|
147
|
+
))}
|
|
148
|
+
</tbody>
|
|
149
|
+
</table>
|
|
150
|
+
),
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<DetailPanel title={hook.name || hook.url} subtitle={hook.name ? hook.url : undefined} badges={<StatusPill tone={active ? "positive" : "neutral"}>{active ? "active" : "paused"}</StatusPill>} actions={extraActions ? <>{actions}{extraActions}</> : actions} tabs={extraTabs?.length ? [...tabs, ...extraTabs] : tabs} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} />
|
|
156
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
157
|
+
<ConfirmSpendDialog {...dialog} />
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -27,10 +27,14 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
27
27
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
28
28
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
29
29
|
import { arrayOf, formatWhen } from "../lib/integrations-utils";
|
|
30
|
+
import { plural } from "@/g8/lib/plural";
|
|
31
|
+
import { DEFAULT_BRAND, useBrand } from "@/g8/hooks/use-brand";
|
|
30
32
|
|
|
31
33
|
export interface WebhookManagerProps {
|
|
32
34
|
/** Pre-fill the endpoint URL of the create form (your /api/webhooks/graph8 route). */
|
|
33
35
|
defaultUrl?: string;
|
|
36
|
+
/** Called when a person opens a webhook (show a WebhookDetail). */
|
|
37
|
+
onSelectWebhook?: (webhook: { id: string; url: string; name?: string | null }) => void;
|
|
34
38
|
className?: string;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -65,13 +69,13 @@ type Pending =
|
|
|
65
69
|
| { kind: "rotate"; hook: Webhook }
|
|
66
70
|
| { kind: "delete"; hook: Webhook };
|
|
67
71
|
|
|
68
|
-
function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: string; label: string } {
|
|
72
|
+
function copyFor(p: Pending, platform: string = DEFAULT_BRAND.linkLabel): { tier: ConfirmTier; title: string; description: string; label: string } {
|
|
69
73
|
switch (p.kind) {
|
|
70
74
|
case "create":
|
|
71
75
|
return {
|
|
72
76
|
tier: "external",
|
|
73
|
-
title: `Send ${p.events.length
|
|
74
|
-
description:
|
|
77
|
+
title: `Send ${plural(p.events.length, "event type")} to ${p.url}`,
|
|
78
|
+
description: `${platform} will POST signed events to this URL as they happen.`,
|
|
75
79
|
label: "Create webhook",
|
|
76
80
|
};
|
|
77
81
|
case "toggle":
|
|
@@ -80,7 +84,7 @@ function copyFor(p: Pending): { tier: ConfirmTier; title: string; description: s
|
|
|
80
84
|
title: `${p.hook.is_active === false ? "Resume" : "Pause"} deliveries to ${p.hook.url}`,
|
|
81
85
|
description:
|
|
82
86
|
p.hook.is_active === false
|
|
83
|
-
?
|
|
87
|
+
? `${platform} starts sending events to this URL again.`
|
|
84
88
|
: "Events stop being sent to this URL until you resume.",
|
|
85
89
|
label: p.hook.is_active === false ? "Resume" : "Pause",
|
|
86
90
|
};
|
|
@@ -133,7 +137,8 @@ function Deliveries({ hook }: { hook: Webhook }) {
|
|
|
133
137
|
);
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
export function WebhookManager({ defaultUrl = "", className }: WebhookManagerProps) {
|
|
140
|
+
export function WebhookManager({ defaultUrl = "", onSelectWebhook, className }: WebhookManagerProps) {
|
|
141
|
+
const brand = useBrand();
|
|
137
142
|
const hooks = useG8Query((signal) => g8Get("/webhooks", undefined, { signal }), []);
|
|
138
143
|
const events = useG8Query((signal) => g8Get("/webhooks/events", undefined, { signal }), []);
|
|
139
144
|
const [url, setUrl] = useState(defaultUrl);
|
|
@@ -169,7 +174,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
169
174
|
);
|
|
170
175
|
const urlOk = /^https:\/\/\S+$/.test(url.trim());
|
|
171
176
|
const toggle = (event: string) => setChosen((c) => (c.includes(event) ? c.filter((x) => x !== event) : [...c, event]));
|
|
172
|
-
const confirm = pending ? copyFor(pending) : null;
|
|
177
|
+
const confirm = pending ? copyFor(pending, brand.linkLabel) : null;
|
|
173
178
|
|
|
174
179
|
return (
|
|
175
180
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
@@ -178,7 +183,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
178
183
|
<div className="grid gap-2 md:grid-cols-2">
|
|
179
184
|
<Input
|
|
180
185
|
aria-label="Endpoint URL"
|
|
181
|
-
placeholder=
|
|
186
|
+
placeholder={`https://yourapp.com/api/webhooks/${brand.linkLabel.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}`}
|
|
182
187
|
value={url}
|
|
183
188
|
onChange={(e) => setUrl(e.target.value)}
|
|
184
189
|
/>
|
|
@@ -238,7 +243,7 @@ export function WebhookManager({ defaultUrl = "", className }: WebhookManagerPro
|
|
|
238
243
|
<li key={h.id} className="space-y-2 px-3 py-3">
|
|
239
244
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
240
245
|
<div>
|
|
241
|
-
<div className="font-medium">{h.name || h.url}</div>
|
|
246
|
+
{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>}
|
|
242
247
|
<div className="text-xs text-gray-500">
|
|
243
248
|
{h.name ? `${h.url} - ` : ""}
|
|
244
249
|
{(h.events ?? []).length} events - {h.is_active === false ? "paused" : "active"}
|