@graph8/sdk 0.308.1 → 0.357.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 +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "analytics",
|
|
4
|
+
"title": "Analytics & reports",
|
|
5
|
+
"summary": "Outbound KPI tiles, rep leaderboard, funnel charts and a builder for saved custom-object reports.",
|
|
6
|
+
"features": [
|
|
7
|
+
"top-line outbound KPIs with a time range",
|
|
8
|
+
"rep leaderboard ranked by reply rate",
|
|
9
|
+
"meeting and revenue funnels without a chart library",
|
|
10
|
+
"build, preview, save, run and delete custom-object reports",
|
|
11
|
+
"stale-cache notices instead of silent old numbers"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"foundation"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"archive_object_report_api_reports__report_id__delete",
|
|
18
|
+
"create_object_report_api_reports_post",
|
|
19
|
+
"execute_object_report_api_reports__report_id__execute_post",
|
|
20
|
+
"get_meeting_funnel_analytics_outbound_meeting_funnel_get",
|
|
21
|
+
"get_outbound_summary_analytics_outbound_summary_get",
|
|
22
|
+
"get_sdr_leaderboard_analytics_outbound_sdr_activity_leaderboard_get",
|
|
23
|
+
"get_team_conversion_funnel_analytics_team_conversion_funnel_get",
|
|
24
|
+
"list_object_reports_api_reports_get",
|
|
25
|
+
"preview_object_report_api_reports_preview_post",
|
|
26
|
+
"update_object_report_api_reports__report_id__patch"
|
|
27
|
+
],
|
|
28
|
+
"components": [
|
|
29
|
+
{
|
|
30
|
+
"name": "KpiTiles",
|
|
31
|
+
"file": "components/kpi-tiles.tsx",
|
|
32
|
+
"purpose": "Emails, reply rate, calls and meetings for a 7, 30 or 90 day window.",
|
|
33
|
+
"operations": [
|
|
34
|
+
"get_outbound_summary_analytics_outbound_summary_get"
|
|
35
|
+
],
|
|
36
|
+
"tiers": [
|
|
37
|
+
"read"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "Leaderboard",
|
|
42
|
+
"file": "components/leaderboard.tsx",
|
|
43
|
+
"purpose": "Reps ranked by reply rate with emails, replies and meetings, sortable.",
|
|
44
|
+
"operations": [
|
|
45
|
+
"get_sdr_leaderboard_analytics_outbound_sdr_activity_leaderboard_get"
|
|
46
|
+
],
|
|
47
|
+
"tiers": [
|
|
48
|
+
"read"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "FunnelChart",
|
|
53
|
+
"file": "components/funnel-chart.tsx",
|
|
54
|
+
"purpose": "Meeting funnel or revenue conversion funnel as bars with step conversion.",
|
|
55
|
+
"operations": [
|
|
56
|
+
"get_meeting_funnel_analytics_outbound_meeting_funnel_get",
|
|
57
|
+
"get_team_conversion_funnel_analytics_team_conversion_funnel_get"
|
|
58
|
+
],
|
|
59
|
+
"tiers": [
|
|
60
|
+
"read"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "ReportBuilder",
|
|
65
|
+
"file": "components/report-builder.tsx",
|
|
66
|
+
"purpose": "Define, preview, save, re-run and delete (confirmed) custom-object reports.",
|
|
67
|
+
"operations": [
|
|
68
|
+
"archive_object_report_api_reports__report_id__delete",
|
|
69
|
+
"create_object_report_api_reports_post",
|
|
70
|
+
"execute_object_report_api_reports__report_id__execute_post",
|
|
71
|
+
"list_object_reports_api_reports_get",
|
|
72
|
+
"preview_object_report_api_reports_preview_post",
|
|
73
|
+
"update_object_report_api_reports__report_id__patch"
|
|
74
|
+
],
|
|
75
|
+
"tiers": [
|
|
76
|
+
"read",
|
|
77
|
+
"write",
|
|
78
|
+
"destructive"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"name": "useReport",
|
|
85
|
+
"file": "hooks/use-report.ts",
|
|
86
|
+
"purpose": "Load an analytics report and unwrap the app's success/data/meta wrapper, exposing stale.",
|
|
87
|
+
"operations": [
|
|
88
|
+
"get_meeting_funnel_analytics_outbound_meeting_funnel_get",
|
|
89
|
+
"get_outbound_summary_analytics_outbound_summary_get",
|
|
90
|
+
"get_sdr_leaderboard_analytics_outbound_sdr_activity_leaderboard_get",
|
|
91
|
+
"get_team_conversion_funnel_analytics_team_conversion_funnel_get"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
"lib": [
|
|
96
|
+
{
|
|
97
|
+
"file": "lib/analytics-types.ts",
|
|
98
|
+
"purpose": "Report shapes, reportBody/reportMeta unwrapping and number/percent formatting."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"file": "lib/use-debounced.ts",
|
|
102
|
+
"purpose": "useDebounced: a value that settles after the user stops typing, so inputs do not fire a request per keystroke."
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"recipes": [
|
|
106
|
+
{
|
|
107
|
+
"file": "recipes/01-weekly-outbound-report.ts",
|
|
108
|
+
"title": "Weekly outbound report",
|
|
109
|
+
"when": "A Monday summary: KPIs, rep leaderboard, meeting funnel; read-only."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"file": "recipes/02-revenue-snapshot.ts",
|
|
113
|
+
"title": "Revenue snapshot",
|
|
114
|
+
"when": "Team dashboard, conversion funnel and forecast for a board-pack summary."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"file": "recipes/03-saved-report.ts",
|
|
118
|
+
"title": "Define, preview, save and run a custom-object report",
|
|
119
|
+
"when": "Reporting on a custom object; the feature is per-org."
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"prerequisites": [
|
|
123
|
+
{
|
|
124
|
+
"id": "analytics-scope",
|
|
125
|
+
"description": "Key scope analytics:read for the KPI tiles, leaderboard and funnels; objects:read, objects:write and objects:delete for ReportBuilder.",
|
|
126
|
+
"where": "api",
|
|
127
|
+
"verify": "`g8.api.me.describeCurrentKey()` lists analytics:read (and objects:* if you use ReportBuilder)."
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "reports-switch",
|
|
131
|
+
"description": "Custom-object reports are switched on per organisation in graph8; while off every /reports call is a 404 and ReportBuilder says so. Ask graph8 support or enable it in the app.",
|
|
132
|
+
"where": "app",
|
|
133
|
+
"verify": "GET /reports returns 200 (not 404)."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "data-flowing",
|
|
137
|
+
"description": "Charts are empty until the org has sent email, placed calls or booked meetings. There is nothing to configure; do not fake data.",
|
|
138
|
+
"where": "api",
|
|
139
|
+
"verify": "GET /analytics/outbound/summary shows non-zero emails_sent for the window."
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"scopes": [
|
|
143
|
+
"analytics:read",
|
|
144
|
+
"objects:delete",
|
|
145
|
+
"objects:read",
|
|
146
|
+
"objects:write"
|
|
147
|
+
],
|
|
148
|
+
"defaults": [
|
|
149
|
+
{
|
|
150
|
+
"decision": "Default time window",
|
|
151
|
+
"default": "30 days with 7 and 90 as the alternatives",
|
|
152
|
+
"askHuman": false
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"decision": "Which leaderboard",
|
|
156
|
+
"default": "Outbound reply-rate ranking (reps with at least 10 emails); the dialer's call leaderboard is a separate report",
|
|
157
|
+
"askHuman": false
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"decision": "Which funnel",
|
|
161
|
+
"default": "Meetings (booked, still on calendar, attended); pass source revenue for account-to-deal conversion",
|
|
162
|
+
"askHuman": false
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"decision": "Chart library",
|
|
166
|
+
"default": "None: bars are plain divs; use @graph8/sdk/ui's chart primitives if you want line charts",
|
|
167
|
+
"askHuman": false
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"decision": "Who may see saved reports",
|
|
171
|
+
"default": "Private (visibility private); sharing with the org needs an admin key",
|
|
172
|
+
"askHuman": true
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"peerDependencies": {
|
|
176
|
+
"react": ">=18"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FunnelChart: horizontal funnel bars with the count and the step-to-step conversion.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations (pick the source):
|
|
7
|
+
* source "meetings": GET /analytics/outbound/meeting-funnel?days= (analytics.getMeetingFunnel) read
|
|
8
|
+
* booked -> still on the calendar -> attended (from the bookings table)
|
|
9
|
+
* source "revenue": GET /analytics/team/conversion-funnel?period_days= (revenueAnalytics.getTeamConversionFunnel) read
|
|
10
|
+
* accounts -> contacted -> ... -> won, with the overall conversion rate
|
|
11
|
+
* Or pass your own `steps` and no request is made. Drawn with plain divs: no chart library is needed.
|
|
12
|
+
*/
|
|
13
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
|
+
import { useReport } from "../hooks/use-report";
|
|
17
|
+
import { formatNumber, formatPercent, type FunnelStep } from "../lib/analytics-types";
|
|
18
|
+
|
|
19
|
+
export interface FunnelChartProps {
|
|
20
|
+
source?: "meetings" | "revenue";
|
|
21
|
+
/** Window in days. */
|
|
22
|
+
days?: number;
|
|
23
|
+
/** Bring your own steps instead of fetching. */
|
|
24
|
+
steps?: FunnelStep[];
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MeetingFunnel {
|
|
29
|
+
summary?: { meetings_booked?: number; meetings_active?: number; meetings_attended?: number };
|
|
30
|
+
}
|
|
31
|
+
interface RevenueFunnel {
|
|
32
|
+
funnel_stages?: Array<{ stage_name: string; stage_label: string; count: number; conversion_rate?: number | null }>;
|
|
33
|
+
overall_conversion_rate?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stepsFrom(source: "meetings" | "revenue", data: unknown): FunnelStep[] {
|
|
37
|
+
if (source === "revenue") {
|
|
38
|
+
return ((data as RevenueFunnel)?.funnel_stages ?? []).map((s) => ({ key: s.stage_name, label: s.stage_label, count: s.count, conversion: s.conversion_rate }));
|
|
39
|
+
}
|
|
40
|
+
const s = (data as MeetingFunnel)?.summary;
|
|
41
|
+
if (!s) return [];
|
|
42
|
+
const booked = s.meetings_booked ?? 0;
|
|
43
|
+
const pct = (n?: number, of?: number) => (of ? ((n ?? 0) / of) * 100 : null);
|
|
44
|
+
return [
|
|
45
|
+
{ key: "booked", label: "Booked", count: booked },
|
|
46
|
+
{ key: "active", label: "Still on calendar", count: s.meetings_active ?? 0, conversion: pct(s.meetings_active, booked) },
|
|
47
|
+
{ key: "attended", label: "Attended", count: s.meetings_attended ?? 0, conversion: pct(s.meetings_attended, s.meetings_active) },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function FunnelChart({ source = "meetings", days = 30, steps: given, className }: FunnelChartProps) {
|
|
52
|
+
const path = source === "revenue" ? "/analytics/team/conversion-funnel" : "/analytics/outbound/meeting-funnel";
|
|
53
|
+
const { data, loading, error, refetch } = useReport<unknown>(path, source === "revenue" ? { period_days: days } : { days }, { enabled: !given });
|
|
54
|
+
const steps = given ?? stepsFrom(source, data);
|
|
55
|
+
const max = Math.max(1, ...steps.map((s) => s.count));
|
|
56
|
+
|
|
57
|
+
if (!given && error) return <ApiError error={error} title="Could not load the funnel" onRetry={() => void refetch()} />;
|
|
58
|
+
if (!given && loading && !data) return <Skeleton className="h-40 w-full" aria-label="Loading funnel" />;
|
|
59
|
+
if (steps.length === 0 || steps[0].count === 0) return <EmptyState className={className} title="Nothing to chart yet" description="The funnel fills in as activity is recorded in this window." />;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ol className={`space-y-2 ${className ?? ""}`} aria-label="Funnel">
|
|
63
|
+
{steps.map((s) => (
|
|
64
|
+
<li key={s.key} className="space-y-1">
|
|
65
|
+
<div className="flex justify-between text-sm">
|
|
66
|
+
<span className="font-medium">{s.label}</span>
|
|
67
|
+
<span>{formatNumber(s.count)}{s.conversion != null ? <span className="ml-2 text-muted-foreground">{formatPercent(s.conversion)} of previous</span> : null}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="h-6 rounded bg-muted" role="img" aria-label={`${s.label}: ${s.count}`}>
|
|
70
|
+
<div className="h-6 rounded bg-primary" style={{ width: `${Math.max(2, (s.count / max) * 100)}%` }} />
|
|
71
|
+
</div>
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
{!given && source === "revenue" && (data as RevenueFunnel | undefined)?.overall_conversion_rate != null ? (
|
|
75
|
+
<li className="text-sm text-muted-foreground">Overall conversion {formatPercent((data as RevenueFunnel).overall_conversion_rate)}</li>
|
|
76
|
+
) : null}
|
|
77
|
+
</ol>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KpiTiles: the top-line outbound numbers (emails, replies, calls, meetings) for a chosen window.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: GET /analytics/outbound/summary?days= (analytics.getOutboundSummary) read
|
|
7
|
+
* Rates in the response are percentages. Meetings come from the bookings table (the canonical count), so
|
|
8
|
+
* `meetings_active` is meetings still on the calendar. If the API answers from a stale cache a notice is shown.
|
|
9
|
+
* Pass `tiles` to choose different fields from the summary.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { useReport } from "../hooks/use-report";
|
|
15
|
+
import { DAY_RANGES, formatNumber, formatPercent, type OutboundSummary } from "../lib/analytics-types";
|
|
16
|
+
|
|
17
|
+
export interface KpiTile {
|
|
18
|
+
key: keyof OutboundSummary & string;
|
|
19
|
+
label: string;
|
|
20
|
+
kind?: "number" | "percent";
|
|
21
|
+
hint?: (s: OutboundSummary) => string | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_TILES: KpiTile[] = [
|
|
25
|
+
{ key: "emails_sent", label: "Emails sent", hint: (s) => `${formatPercent(s.delivery_rate as number | undefined)} delivered` },
|
|
26
|
+
{ key: "reply_rate", label: "Reply rate", kind: "percent", hint: (s) => `${formatNumber(s.emails_replied as number | undefined)} replies` },
|
|
27
|
+
{ key: "calls_dispatched", label: "Calls placed", hint: (s) => `${formatPercent(s.connect_rate as number | undefined)} connected` },
|
|
28
|
+
{ key: "meetings_booked", label: "Meetings booked", hint: (s) => `${formatNumber(s.meetings_active as number | undefined)} still on the calendar` },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export interface KpiTilesProps {
|
|
32
|
+
/** Initial window in days. */
|
|
33
|
+
days?: number;
|
|
34
|
+
tiles?: KpiTile[];
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function KpiTiles({ days: initialDays = 30, tiles = DEFAULT_TILES, className }: KpiTilesProps) {
|
|
39
|
+
const [days, setDays] = useState(initialDays);
|
|
40
|
+
const { data, stale, loading, error, refetch } = useReport<OutboundSummary>("/analytics/outbound/summary", { days });
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<section className={`space-y-3 ${className ?? ""}`} aria-label="Key metrics">
|
|
44
|
+
<div role="group" aria-label="Time range" className="flex gap-1">
|
|
45
|
+
{DAY_RANGES.map((d) => (
|
|
46
|
+
<Button key={d} size="sm" variant={days === d ? "default" : "outline"} onClick={() => setDays(d)}>
|
|
47
|
+
Last {d} days
|
|
48
|
+
</Button>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
{error ? (
|
|
52
|
+
<ApiError error={error} title="Could not load metrics" onRetry={() => void refetch()} />
|
|
53
|
+
) : loading && !data ? (
|
|
54
|
+
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4" aria-busy="true" aria-label="Loading metrics">
|
|
55
|
+
{tiles.map((t) => <Skeleton key={t.key} className="h-24" />)}
|
|
56
|
+
</div>
|
|
57
|
+
) : (
|
|
58
|
+
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
59
|
+
{tiles.map((t) => {
|
|
60
|
+
const raw = data?.[t.key] as number | undefined;
|
|
61
|
+
return (
|
|
62
|
+
<div key={t.key} className="rounded-lg border p-4">
|
|
63
|
+
<div className="text-xs uppercase text-muted-foreground">{t.label}</div>
|
|
64
|
+
<div className="mt-1 text-2xl font-semibold">{t.kind === "percent" ? formatPercent(raw) : formatNumber(raw)}</div>
|
|
65
|
+
{data && t.hint ? <div className="mt-1 text-xs text-muted-foreground">{t.hint(data)}</div> : null}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
{stale ? <p role="status" className="text-xs text-muted-foreground">These numbers are from a recent cache because a refresh failed.</p> : null}
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Leaderboard: reps ranked by reply rate, with emails, replies and meetings booked.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: GET /analytics/outbound/sdr-activity/leaderboard?days=&limit= (analytics.getSdrLeaderboard) read
|
|
7
|
+
* A rep only appears after sending at least 10 emails (the app's floor), so a rep with three sends and one reply is
|
|
8
|
+
* not ranked at 33%. Warming mailboxes are collapsed to the person who owns them. This is the OUTBOUND ranking; the
|
|
9
|
+
* dialer's call leaderboard is a different report (`GET /analytics/dialer/leaderboard`).
|
|
10
|
+
* Rows are sorted client-side by the column you click; `rep` shows the email unless you pass `nameFor`.
|
|
11
|
+
*/
|
|
12
|
+
import { useMemo, useState } from "react";
|
|
13
|
+
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
16
|
+
import { useReport } from "../hooks/use-report";
|
|
17
|
+
import { DAY_RANGES, formatNumber, formatPercent, type LeaderRow } from "../lib/analytics-types";
|
|
18
|
+
|
|
19
|
+
type SortKey = "reply_rate" | "emails_sent" | "replies" | "meetings_booked";
|
|
20
|
+
|
|
21
|
+
export interface LeaderboardProps {
|
|
22
|
+
days?: number;
|
|
23
|
+
limit?: number;
|
|
24
|
+
/** Map an email to a display name. */
|
|
25
|
+
nameFor?: (email: string) => string;
|
|
26
|
+
onSelectRep?: (email: string) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COLUMNS: ReadonlyArray<{ key: SortKey; label: string; percent?: boolean }> = [
|
|
31
|
+
{ key: "reply_rate", label: "Reply rate", percent: true },
|
|
32
|
+
{ key: "emails_sent", label: "Emails sent" },
|
|
33
|
+
{ key: "replies", label: "Replies" },
|
|
34
|
+
{ key: "meetings_booked", label: "Meetings" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function Leaderboard({ days: initialDays = 30, limit = 20, nameFor, onSelectRep, className }: LeaderboardProps) {
|
|
38
|
+
const [days, setDays] = useState(initialDays);
|
|
39
|
+
const [sortKey, setSortKey] = useState<SortKey>("reply_rate");
|
|
40
|
+
const { data, stale, loading, error, refetch } = useReport<LeaderRow[]>("/analytics/outbound/sdr-activity/leaderboard", { days, limit });
|
|
41
|
+
const rows = useMemo(() => [...(Array.isArray(data) ? data : [])].sort((a, b) => Number(b[sortKey] ?? 0) - Number(a[sortKey] ?? 0)), [data, sortKey]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<section className={`space-y-3 ${className ?? ""}`} aria-label="Rep leaderboard">
|
|
45
|
+
<div role="group" aria-label="Time range" className="flex gap-1">
|
|
46
|
+
{DAY_RANGES.map((d) => <Button key={d} size="sm" variant={days === d ? "default" : "outline"} onClick={() => setDays(d)}>Last {d} days</Button>)}
|
|
47
|
+
</div>
|
|
48
|
+
{error ? (
|
|
49
|
+
<ApiError error={error} title="Could not load the leaderboard" onRetry={() => void refetch()} />
|
|
50
|
+
) : loading && !data ? (
|
|
51
|
+
<Skeleton className="h-40 w-full" aria-label="Loading leaderboard" />
|
|
52
|
+
) : rows.length === 0 ? (
|
|
53
|
+
<EmptyState title="No ranked reps yet" description="A rep appears here after sending at least 10 emails in the window." />
|
|
54
|
+
) : (
|
|
55
|
+
<Table>
|
|
56
|
+
<TableHeader>
|
|
57
|
+
<TableRow>
|
|
58
|
+
<TableHead className="w-10">#</TableHead>
|
|
59
|
+
<TableHead>Rep</TableHead>
|
|
60
|
+
{COLUMNS.map((c) => (
|
|
61
|
+
<TableHead key={c.key} className="text-right" aria-sort={sortKey === c.key ? "descending" : "none"}>
|
|
62
|
+
<button type="button" className="font-medium hover:underline" onClick={() => setSortKey(c.key)}>{c.label}</button>
|
|
63
|
+
</TableHead>
|
|
64
|
+
))}
|
|
65
|
+
</TableRow>
|
|
66
|
+
</TableHeader>
|
|
67
|
+
<TableBody>
|
|
68
|
+
{rows.map((r, i) => {
|
|
69
|
+
const email = r.sdr_email ?? "unknown";
|
|
70
|
+
return (
|
|
71
|
+
<TableRow key={email} className={onSelectRep ? "cursor-pointer" : undefined} onClick={() => onSelectRep?.(email)}>
|
|
72
|
+
<TableCell>{i + 1}</TableCell>
|
|
73
|
+
<TableCell className="font-medium">{nameFor ? nameFor(email) : email}</TableCell>
|
|
74
|
+
{COLUMNS.map((c) => (
|
|
75
|
+
<TableCell key={c.key} className="text-right">{c.percent ? formatPercent(r[c.key] as number | undefined) : formatNumber(r[c.key] as number | undefined)}</TableCell>
|
|
76
|
+
))}
|
|
77
|
+
</TableRow>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</TableBody>
|
|
81
|
+
</Table>
|
|
82
|
+
)}
|
|
83
|
+
{stale ? <p role="status" className="text-xs text-muted-foreground">Showing cached numbers because a refresh failed.</p> : null}
|
|
84
|
+
</section>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReportBuilder: build a custom-object report (count, sum, average by a field or by time), preview it, save it,
|
|
5
|
+
* and open, re-run or delete saved reports.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /reports?object_slug= (reports.listObjectReportsApi) read - saved reports
|
|
9
|
+
* 2. POST /reports/preview (reports.previewObjectReportApi) write - runs an UNSAVED definition
|
|
10
|
+
* 3. POST /reports (reports.createObjectReportApi) write - save
|
|
11
|
+
* PATCH /reports/{id} { expected_revision } (reports.updateObjectReportApi) write - used when re-saving a loaded report
|
|
12
|
+
* 4. POST /reports/{id}/execute (reports.executeObjectReportApi) write - run a saved report
|
|
13
|
+
* 5. DELETE /reports/{id} (reports.archiveObjectReportApi) DESTRUCTIVE -> ConfirmSpendDialog first
|
|
14
|
+
* These reports run over CUSTOM OBJECTS and process lists (the `objects` block): `object_slug` is the object's slug.
|
|
15
|
+
* The feature is switched on per organisation; while it is off every call is a 404 and the component says so.
|
|
16
|
+
* Preview and run share the org's report-run slots (429 `too_many_report_runs` when all are busy: retry shortly).
|
|
17
|
+
* "write" tier on preview/execute is the contract's classification (they create run records); nothing is sent.
|
|
18
|
+
*/
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { useDebounced } from "../lib/use-debounced";
|
|
28
|
+
import { reportBody, type ReportResultData, type ResultRow, type SavedReport } from "../lib/analytics-types";
|
|
29
|
+
|
|
30
|
+
export interface ReportBuilderProps {
|
|
31
|
+
/** Pre-select the object (its slug), for example from the objects block. */
|
|
32
|
+
objectSlug?: string;
|
|
33
|
+
onSaved?: (report: SavedReport) => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MEASURES = ["count", "sum", "avg", "min", "max"] as const;
|
|
38
|
+
const GRAINS = ["day", "week", "month", "quarter"] as const;
|
|
39
|
+
const CHARTS = ["table", "bar", "line"] as const;
|
|
40
|
+
|
|
41
|
+
interface Draft {
|
|
42
|
+
title: string;
|
|
43
|
+
chart: (typeof CHARTS)[number];
|
|
44
|
+
measure: (typeof MEASURES)[number];
|
|
45
|
+
measureField: string;
|
|
46
|
+
groupBy: string;
|
|
47
|
+
grain: (typeof GRAINS)[number];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Cfg = { measures?: Array<Record<string, unknown>>; group_by?: Array<Record<string, unknown>>; [k: string]: unknown };
|
|
51
|
+
|
|
52
|
+
/** The config to send. A loaded report keeps everything the form cannot edit (filters, extra measures and groups). */
|
|
53
|
+
function configOf(d: Draft, base: Cfg = {}): Record<string, unknown> {
|
|
54
|
+
const measure = { kind: d.measure, ...(d.measure === "count" ? {} : { attribute_slug: d.measureField }) };
|
|
55
|
+
const g = d.groupBy.trim();
|
|
56
|
+
const group = g === "created_at" ? { kind: "created_at", grain: d.grain } : g ? { kind: "attribute", attribute_slug: g } : null;
|
|
57
|
+
return { filters: [], ...base, measures: [measure, ...(base.measures ?? []).slice(1)], group_by: [...(group ? [group] : []), ...(base.group_by ?? []).slice(1)] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Fill the form from a saved report so Preview and Save changes act on ITS definition, not on form defaults. */
|
|
61
|
+
function draftFromReport(r: SavedReport, current: Draft): Draft {
|
|
62
|
+
const cfg = r.config as Cfg;
|
|
63
|
+
const m = cfg.measures?.[0];
|
|
64
|
+
const g = cfg.group_by?.[0];
|
|
65
|
+
const measureKinds = MEASURES as readonly string[];
|
|
66
|
+
return {
|
|
67
|
+
...current,
|
|
68
|
+
title: r.title,
|
|
69
|
+
chart: (r.chart_type as Draft["chart"]) ?? "table",
|
|
70
|
+
measure: m && measureKinds.includes(String(m.kind)) ? (m.kind as Draft["measure"]) : "count",
|
|
71
|
+
measureField: String(m?.attribute_slug ?? ""),
|
|
72
|
+
groupBy: g ? (g.kind === "created_at" ? "created_at" : String(g.attribute_slug ?? "")) : "",
|
|
73
|
+
grain: g && GRAINS.includes(g.grain as Draft["grain"]) ? (g.grain as Draft["grain"]) : "month",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function groupLabel(g: ResultRow["group"][number]): string {
|
|
78
|
+
if (g == null) return "(none)";
|
|
79
|
+
if (typeof g === "object") return String(g.label ?? g.value ?? "(none)");
|
|
80
|
+
return String(g);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ResultTable({ result }: { result: ReportResultData }) {
|
|
84
|
+
const rows = result.rows ?? [];
|
|
85
|
+
if (result.status === "restricted") return <p role="alert" className="text-sm">You do not have access to every field in this report.</p>;
|
|
86
|
+
if (rows.length === 0) return <EmptyState title="No records match" description="Change the object, field or grouping and preview again." />;
|
|
87
|
+
const measureKeys = Object.keys(rows[0].measures ?? {});
|
|
88
|
+
return (
|
|
89
|
+
<div className="space-y-1">
|
|
90
|
+
<Table aria-label="Report result">
|
|
91
|
+
<TableHeader>
|
|
92
|
+
<TableRow><TableHead>Group</TableHead>{measureKeys.map((k) => <TableHead key={k} className="text-right">{k}</TableHead>)}<TableHead className="text-right">Records</TableHead></TableRow>
|
|
93
|
+
</TableHeader>
|
|
94
|
+
<TableBody>
|
|
95
|
+
{rows.map((r) => (
|
|
96
|
+
<TableRow key={r.key}>
|
|
97
|
+
<TableCell>{r.group.length ? r.group.map(groupLabel).join(" / ") : "All records"}</TableCell>
|
|
98
|
+
{measureKeys.map((k) => <TableCell key={k} className="text-right">{String(r.measures[k] ?? "")}</TableCell>)}
|
|
99
|
+
<TableCell className="text-right">{r.record_count ?? ""}</TableCell>
|
|
100
|
+
</TableRow>
|
|
101
|
+
))}
|
|
102
|
+
</TableBody>
|
|
103
|
+
</Table>
|
|
104
|
+
{result.truncated ? <p className="text-xs text-muted-foreground">Showing the first rows only; narrow the report to see the rest.</p> : null}
|
|
105
|
+
{(result.disclosures ?? []).map((d) => <p key={d} className="text-xs text-muted-foreground">{d}</p>)}
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function ReportBuilder({ objectSlug: initialSlug = "", onSaved, className }: ReportBuilderProps) {
|
|
111
|
+
const [slug, setSlug] = useState(initialSlug);
|
|
112
|
+
const [draft, setDraft] = useState<Draft>({ title: "", chart: "table", measure: "count", measureField: "", groupBy: "", grain: "month" });
|
|
113
|
+
const [loaded, setLoaded] = useState<SavedReport | null>(null);
|
|
114
|
+
const [result, setResult] = useState<ReportResultData | null>(null);
|
|
115
|
+
const [deleting, setDeleting] = useState<SavedReport | null>(null);
|
|
116
|
+
const set = <K extends keyof Draft>(k: K, v: Draft[K]) => setDraft((d) => ({ ...d, [k]: v }));
|
|
117
|
+
|
|
118
|
+
const listSlug = useDebounced(slug.trim());
|
|
119
|
+
const saved = useG8Query(async (signal) => reportBody<{ reports?: SavedReport[] }>(await g8Get("/reports", { object_slug: listSlug || undefined, limit: 50 }, { signal })).reports ?? [], [listSlug]);
|
|
120
|
+
const body = () => ({ object_slug: slug, chart_type: draft.chart, config: configOf(draft, loaded?.config as Cfg) });
|
|
121
|
+
const preview = useG8Mutation(async () => { const r = reportBody<ReportResultData>(await g8Post("/reports/preview", body())); setResult(r); return r; });
|
|
122
|
+
const run = useG8Mutation(async (r: SavedReport) => { const out = reportBody<ReportResultData>(await g8Post(`/reports/${encodeURIComponent(r.id)}/execute`, {})); setResult(out); return out; });
|
|
123
|
+
const save = useG8Mutation(async () => {
|
|
124
|
+
const env = loaded
|
|
125
|
+
? await g8Patch(`/reports/${encodeURIComponent(loaded.id)}`, { title: draft.title, chart_type: draft.chart, config: configOf(draft, loaded.config as Cfg), expected_revision: loaded.revision })
|
|
126
|
+
: await g8Post("/reports", { ...body(), title: draft.title, visibility: "private" });
|
|
127
|
+
return reportBody<SavedReport>(env);
|
|
128
|
+
}, { onSuccess: (r) => { setLoaded(r); void saved.refetch(); onSaved?.(r); } });
|
|
129
|
+
const remove = useG8Mutation(async (r: SavedReport) => { await g8Delete(`/reports/${encodeURIComponent(r.id)}`); }, {
|
|
130
|
+
onSuccess: () => { if (deleting?.id === loaded?.id) startOver(); setDeleting(null); void saved.refetch(); },
|
|
131
|
+
onError: () => setDeleting(null),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Start over: clears the loaded report, the definition, the preview and any errors.
|
|
135
|
+
const startOver = () => {
|
|
136
|
+
setLoaded(null);
|
|
137
|
+
setResult(null);
|
|
138
|
+
setSlug(initialSlug);
|
|
139
|
+
setDraft({ title: "", chart: "table", measure: "count", measureField: "", groupBy: "", grain: "month" });
|
|
140
|
+
preview.reset();
|
|
141
|
+
run.reset();
|
|
142
|
+
save.reset();
|
|
143
|
+
remove.reset();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const open = (r: SavedReport) => {
|
|
147
|
+
setLoaded(r);
|
|
148
|
+
setSlug(r.source.slug);
|
|
149
|
+
setDraft((d) => draftFromReport(r, d));
|
|
150
|
+
void run.mutate(r);
|
|
151
|
+
};
|
|
152
|
+
const failure = preview.error ?? run.error ?? save.error ?? remove.error;
|
|
153
|
+
const switchedOff = (saved.error as { status?: number } | undefined)?.status === 404;
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className={`grid gap-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)] ${className ?? ""}`}>
|
|
157
|
+
<section aria-label="Saved reports" className="space-y-2">
|
|
158
|
+
<h3 className="text-sm font-semibold">Saved reports</h3>
|
|
159
|
+
{switchedOff ? <EmptyState title="Reports are not enabled" description="Custom-object reports are switched on per organisation in graph8." /> : saved.error ? <ApiError error={saved.error} title="Could not load saved reports" onRetry={() => void saved.refetch()} /> : null}
|
|
160
|
+
{(saved.data ?? []).map((r) => (
|
|
161
|
+
<div key={r.id} className="flex items-center justify-between rounded border p-2 text-sm">
|
|
162
|
+
<button type="button" className="text-left hover:underline" onClick={() => open(r)}>{r.title} <span className="text-muted-foreground">({r.source.plural_noun})</span></button>
|
|
163
|
+
{r.can_delete ? <Button size="sm" variant="ghost" aria-label={`Delete ${r.title}`} onClick={() => setDeleting(r)}>Delete</Button> : null}
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
{!saved.loading && !saved.error && (saved.data ?? []).length === 0 ? <p className="text-sm text-muted-foreground">Nothing saved yet.</p> : null}
|
|
167
|
+
</section>
|
|
168
|
+
|
|
169
|
+
<section aria-label="Report definition" className="space-y-3">
|
|
170
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
171
|
+
<div className="space-y-1"><Label htmlFor="rb-slug">Object slug</Label><Input id="rb-slug" value={slug} onChange={(e) => setSlug(e.target.value)} placeholder="for example projects" /></div>
|
|
172
|
+
<div className="space-y-1"><Label htmlFor="rb-title">Title</Label><Input id="rb-title" value={draft.title} onChange={(e) => set("title", e.target.value)} /></div>
|
|
173
|
+
<div className="space-y-1">
|
|
174
|
+
<Label htmlFor="rb-measure">Measure</Label>
|
|
175
|
+
<div className="flex gap-2">
|
|
176
|
+
<select id="rb-measure" className="h-9 rounded-md border bg-background px-2 text-sm" value={draft.measure} onChange={(e) => set("measure", e.target.value as Draft["measure"])}>{MEASURES.map((m) => <option key={m}>{m}</option>)}</select>
|
|
177
|
+
{draft.measure !== "count" ? <Input aria-label="Measure field" placeholder="numeric field slug" value={draft.measureField} onChange={(e) => set("measureField", e.target.value)} /> : null}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div className="space-y-1">
|
|
181
|
+
<Label htmlFor="rb-group">Group by</Label>
|
|
182
|
+
<div className="flex gap-2">
|
|
183
|
+
<Input id="rb-group" value={draft.groupBy} onChange={(e) => set("groupBy", e.target.value)} placeholder="field slug, or created_at" />
|
|
184
|
+
{draft.groupBy.trim() === "created_at" ? <select aria-label="Grain" className="h-9 rounded-md border bg-background px-2 text-sm" value={draft.grain} onChange={(e) => set("grain", e.target.value as Draft["grain"])}>{GRAINS.map((g) => <option key={g}>{g}</option>)}</select> : null}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="space-y-1"><Label htmlFor="rb-chart">Chart</Label><select id="rb-chart" className="h-9 w-full rounded-md border bg-background px-2 text-sm" value={draft.chart} onChange={(e) => set("chart", e.target.value as Draft["chart"])}>{CHARTS.map((c) => <option key={c}>{c}</option>)}</select></div>
|
|
188
|
+
</div>
|
|
189
|
+
{failure ? <ApiError error={failure} title="That report action failed" /> : null}
|
|
190
|
+
<div className="flex gap-2">
|
|
191
|
+
<Button variant="ghost" onClick={startOver}>New report</Button>
|
|
192
|
+
<Button variant="outline" disabled={!slug || preview.loading} loading={preview.loading} onClick={() => void preview.mutate(undefined)}>Preview</Button>
|
|
193
|
+
<Button disabled={!slug || !draft.title.trim() || save.loading} loading={save.loading} onClick={() => void save.mutate(undefined)}>{loaded ? "Save changes" : "Save report"}</Button>
|
|
194
|
+
</div>
|
|
195
|
+
{result ? <ResultTable result={result} /> : null}
|
|
196
|
+
</section>
|
|
197
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.title}"?`} description="The saved report is removed for everyone who can see it. The underlying records are not touched." confirmLabel="Delete report" affected={1} confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting)} onCancel={() => setDeleting(null)} />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useReport: load one analytics report and unwrap the app's `{ success, data, meta }` wrapper.
|
|
5
|
+
*
|
|
6
|
+
* Pass the path relative to /api/v1 and its query. Re-fetches when the path or query change. `stale` is true when
|
|
7
|
+
* the numbers came from a cache after a failed refresh.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations: any GET report under /analytics (read tier). Components pass the exact path they use.
|
|
10
|
+
*/
|
|
11
|
+
import { g8Get, type G8QueryValue } from "@/g8/lib/g8-client";
|
|
12
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
13
|
+
import { reportBody, reportMeta, type ReportMeta } from "../lib/analytics-types";
|
|
14
|
+
|
|
15
|
+
export function useReport<T>(path: string, query: Record<string, G8QueryValue>, options: { enabled?: boolean } = {}) {
|
|
16
|
+
const key = JSON.stringify(query);
|
|
17
|
+
const q = useG8Query(
|
|
18
|
+
async (signal) => {
|
|
19
|
+
const env = await g8Get(path, query, { signal });
|
|
20
|
+
return { body: reportBody<T>(env), meta: reportMeta(env) as ReportMeta };
|
|
21
|
+
},
|
|
22
|
+
[path, key],
|
|
23
|
+
options,
|
|
24
|
+
);
|
|
25
|
+
return { data: q.data?.body, stale: Boolean(q.data?.meta.stale), loading: q.loading, error: q.error, refetch: q.refetch };
|
|
26
|
+
}
|