@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReportDetail: everything about one saved custom-object report: its definition (object, measure, grouping, filters,
|
|
5
|
+
* chart type, visibility, revision, owner dates), the latest result table, and the actions Run again and Delete.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /reports/{id} (reports.getObjectReportApi) read the definition
|
|
9
|
+
* 2. POST /reports/{id}/execute (reports.executeObjectReportApi) write run it (creates a run record, sends nothing); runs once on open
|
|
10
|
+
* 3. DELETE /reports/{id} (reports.archiveObjectReportApi) DESTRUCTIVE -> ConfirmSpendDialog
|
|
11
|
+
* Editing happens in ReportBuilder (`reportId`). Preview and execute share the org's report-run slots (429 `too_many_report_runs`).
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
17
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
22
|
+
import { reportBody, type ReportResultData, type SavedReport } from "../lib/analytics-types";
|
|
23
|
+
import { ResultTable } from "./report-builder";
|
|
24
|
+
|
|
25
|
+
export interface ReportDetailProps {
|
|
26
|
+
reportId: string;
|
|
27
|
+
/** Called after the report was deleted. */
|
|
28
|
+
onDeleted?: () => void;
|
|
29
|
+
/** A result the host already holds (kept across tab switches). While set, opening does not run the report again. */
|
|
30
|
+
result?: ReportResultData | null;
|
|
31
|
+
/** Called with every fresh result, so the host can keep it. */
|
|
32
|
+
onResult?: (result: ReportResultData | null) => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Cfg = { measures?: Array<Record<string, unknown>>; group_by?: Array<Record<string, unknown>>; filters?: unknown[] };
|
|
37
|
+
|
|
38
|
+
const measureText = (m: Record<string, unknown> | undefined): string => (m ? `${String(m.kind)}${m.attribute_slug ? ` of ${String(m.attribute_slug)}` : ""}` : "");
|
|
39
|
+
const groupText = (g: Record<string, unknown> | undefined): string => (g ? (g.kind === "created_at" ? `created date by ${String(g.grain ?? "month")}` : String(g.attribute_slug ?? "")) : "None");
|
|
40
|
+
|
|
41
|
+
export function ReportDetail({ reportId, onDeleted, result: given, onResult, className }: ReportDetailProps) {
|
|
42
|
+
const [confirming, setConfirming] = useState(false);
|
|
43
|
+
const [own, setOwn] = useState<ReportResultData | null>(null);
|
|
44
|
+
const result = given ?? own;
|
|
45
|
+
const setResult = (r: ReportResultData | null) => { setOwn(r); onResult?.(r); };
|
|
46
|
+
const report = useG8Query(async (signal) => reportBody<SavedReport>(await g8Get(`/reports/${encodeURIComponent(reportId)}`, undefined, { signal })), [reportId]);
|
|
47
|
+
const run = useG8Mutation(async () => { const out = reportBody<ReportResultData>(await g8Post(`/reports/${encodeURIComponent(reportId)}/execute`, {})); setResult(out); return out; });
|
|
48
|
+
const remove = useG8Mutation(async () => { await g8Delete(`/reports/${encodeURIComponent(reportId)}`); }, { onSuccess: () => { setConfirming(false); onDeleted?.(); }, onError: () => setConfirming(false) });
|
|
49
|
+
|
|
50
|
+
// A different report starts clean; it runs once on open unless the host already holds a result.
|
|
51
|
+
useEffect(() => { run.reset(); if (!given) void run.mutate(undefined); }, [reportId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
52
|
+
|
|
53
|
+
const r = report.data;
|
|
54
|
+
const cfg = (r?.config ?? {}) as Cfg;
|
|
55
|
+
if (report.error) return <ApiError error={report.error} title="Could not load the report" onRetry={() => void report.refetch()} />;
|
|
56
|
+
if (!r) return <Skeleton className="h-40 w-full" aria-label="Loading report" />;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className={`space-y-4 ${className ?? ""}`} data-testid="report-detail">
|
|
60
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
61
|
+
<StatusPill tone={r.visibility === "private" ? "neutral" : "info"}>{r.visibility}</StatusPill>
|
|
62
|
+
<span className="text-xs text-muted-foreground">Revision {r.revision}</span>
|
|
63
|
+
<div className="ml-auto flex gap-2">
|
|
64
|
+
<Button size="sm" variant="outline" loading={run.loading} onClick={() => void run.mutate(undefined)}>Run again</Button>
|
|
65
|
+
{r.can_delete ? <Button size="sm" variant="ghost" onClick={() => setConfirming(true)}>Delete</Button> : null}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<KeyValueList columns={2} items={[
|
|
69
|
+
{ label: "Object", value: r.source.plural_noun, mono: false },
|
|
70
|
+
{ label: "Object slug", value: r.source.slug, mono: true },
|
|
71
|
+
{ label: "Chart", value: r.chart_type },
|
|
72
|
+
{ label: "Measure", value: (cfg.measures ?? []).map(measureText).join(", ") || "count" },
|
|
73
|
+
{ label: "Grouped by", value: (cfg.group_by ?? []).map(groupText).join(", ") || "None" },
|
|
74
|
+
{ label: "Filters", value: (cfg.filters ?? []).length ? `${(cfg.filters ?? []).length} applied` : "None" },
|
|
75
|
+
]} />
|
|
76
|
+
{run.error ? <ApiError error={run.error} title="Could not run the report" onRetry={() => void run.mutate(undefined)} /> : null}
|
|
77
|
+
{run.loading && !result ? <Skeleton className="h-24 w-full" aria-label="Running report" /> : null}
|
|
78
|
+
{result ? <ResultTable result={result} /> : null}
|
|
79
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the report" /> : null}
|
|
80
|
+
<ConfirmSpendDialog open={confirming} tier="destructive" title={`Delete "${r.title}"?`} description="The saved report is removed for everyone who can see it. The underlying records are not touched."
|
|
81
|
+
confirmLabel="Delete report" affected={1} confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setConfirming(false)} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TrendChart: a daily line chart for one outbound channel (email, calls or meetings), drawn with the kit TimeSeriesChart (recharts).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations (pick the channel):
|
|
7
|
+
* channel "email": GET /analytics/outbound/email-metrics/trend?days= (analytics.getEmailTrend) read rows { date, sent, replied, ... }
|
|
8
|
+
* channel "calls": GET /analytics/outbound/call-metrics/trend?days= (analytics.getCallTrend) read rows { date, dials, connected, ... }
|
|
9
|
+
* channel "meetings": GET /analytics/outbound/meeting-metrics/trend?days= (analytics.getMeetingTrend) read rows { date, booked, cancelled, held, ... }
|
|
10
|
+
* Two series per channel (the volume and the outcome). Dates are plain `YYYY-MM-DD` days and are shown as UTC days.
|
|
11
|
+
*/
|
|
12
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
|
+
import { TimeSeriesChart } from "@/g8/components/screen/time-series-chart";
|
|
16
|
+
import { useReport } from "../hooks/use-report";
|
|
17
|
+
import { formatNumber } from "../lib/analytics-types";
|
|
18
|
+
|
|
19
|
+
export type TrendChannel = "email" | "calls" | "meetings";
|
|
20
|
+
|
|
21
|
+
export const TREND_SERIES: Record<TrendChannel, { path: string; keys: [string, string]; labels: [string, string] }> = {
|
|
22
|
+
email: { path: "/analytics/outbound/email-metrics/trend", keys: ["sent", "replied"], labels: ["Sent", "Replied"] },
|
|
23
|
+
calls: { path: "/analytics/outbound/call-metrics/trend", keys: ["dials", "connected"], labels: ["Dials", "Connected"] },
|
|
24
|
+
meetings: { path: "/analytics/outbound/meeting-metrics/trend", keys: ["booked", "held"], labels: ["Booked", "Held"] },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type TrendRow = { date?: string } & Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
/** Daily values of one key, in date order. */
|
|
30
|
+
export function seriesOf(rows: TrendRow[] | undefined, key: string): number[] {
|
|
31
|
+
return (Array.isArray(rows) ? rows : []).map((r) => Number(r[key] ?? 0) || 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TrendChartProps {
|
|
35
|
+
channel?: TrendChannel;
|
|
36
|
+
days?: number;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function TrendChart({ channel = "email", days = 30, className }: TrendChartProps) {
|
|
41
|
+
const spec = TREND_SERIES[channel];
|
|
42
|
+
const { data, loading, error, refetch } = useReport<TrendRow[]>(spec.path, { days });
|
|
43
|
+
const rows = Array.isArray(data) ? data : [];
|
|
44
|
+
const a = seriesOf(rows, spec.keys[0]);
|
|
45
|
+
const b = seriesOf(rows, spec.keys[1]);
|
|
46
|
+
const total = (v: number[]) => v.reduce((x, y) => x + y, 0);
|
|
47
|
+
|
|
48
|
+
if (error) return <ApiError error={error} title="Could not load the trend" onRetry={() => void refetch()} />;
|
|
49
|
+
if (loading && !data) return <Skeleton className="h-48 w-full" aria-label="Loading trend" />;
|
|
50
|
+
if (rows.length === 0 || total(a) === 0) return <EmptyState className={className} title="No activity in this window" description="Daily numbers appear here as emails, calls and meetings are recorded." />;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<figure className={className} aria-label={`${channel} trend`}>
|
|
54
|
+
<div className="mb-2 flex flex-wrap gap-4 text-xs text-muted-foreground">
|
|
55
|
+
<span>{spec.labels[0]} {formatNumber(total(a))}</span>
|
|
56
|
+
<span>{spec.labels[1]} {formatNumber(total(b))}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<TimeSeriesChart data={rows} xKey="date" kind="line" height={200}
|
|
59
|
+
series={[{ key: spec.keys[0], label: spec.labels[0] }, { key: spec.keys[1], label: spec.labels[1], tone: "positive" }]} />
|
|
60
|
+
</figure>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -98,3 +98,9 @@ with the full API key.
|
|
|
98
98
|
- `03-bookings-digest.ts`: upcoming bookings, pending approvals, recorded meetings.
|
|
99
99
|
- `04-calendar-invite.ts`: ad-hoc invite via `calendarInvites.createCalendarInvite` (not part of the block's
|
|
100
100
|
UI; add the operation to your own proxy allowlist if you call it from the browser).
|
|
101
|
+
|
|
102
|
+
## Cross-block glue
|
|
103
|
+
|
|
104
|
+
- `MeetingContactLinks` (`components/meeting-contact-links.tsx`): "Open <attendee>" buttons for a booked or recorded meeting, one per attendee who is a CRM contact. Use it in `MeetingsScreen`'s `renderDetailActions`.
|
|
105
|
+
|
|
106
|
+
- `RecordMeetingsTab` (`components/record-meetings-tab.tsx`): meetings booked with a contact (`getContactMeetings`, the API's 50 most recent booking events), each opening `/meetings?sel=booking:<id>`. `extraDetailTabs` entry for ContactsScreen.
|
|
@@ -23,10 +23,12 @@
|
|
|
23
23
|
"get_availability_appointments_schedules__schedule_id__availability_get",
|
|
24
24
|
"get_available_slots_appointments_slots_get",
|
|
25
25
|
"get_booking_appointments_bookings__booking_uid__get",
|
|
26
|
+
"get_contact_meetings_contacts__contact_id__meetings_get",
|
|
26
27
|
"get_event_type_appointments_event_types__event_type_id__get",
|
|
27
28
|
"get_meeting_inbox_meetings__meeting_id__get",
|
|
28
29
|
"list_bookings_appointments_bookings_get",
|
|
29
30
|
"list_calendars_appointments_calendars_get",
|
|
31
|
+
"list_contacts_contacts_get",
|
|
30
32
|
"list_event_types_event_types_get",
|
|
31
33
|
"list_meetings_inbox_meetings_get",
|
|
32
34
|
"list_schedules_appointments_schedules_get",
|
|
@@ -113,8 +115,12 @@
|
|
|
113
115
|
"name": "MeetingsScreen",
|
|
114
116
|
"file": "components/meetings-screen.tsx",
|
|
115
117
|
"purpose": "Finished meetings product: upcoming, past, cancelled and recorded meetings with detail, approvals, notes and a schedule-a-meeting flow.",
|
|
116
|
-
"operations": [
|
|
117
|
-
|
|
118
|
+
"operations": [
|
|
119
|
+
"list_bookings_appointments_bookings_get"
|
|
120
|
+
],
|
|
121
|
+
"tiers": [
|
|
122
|
+
"read"
|
|
123
|
+
]
|
|
118
124
|
},
|
|
119
125
|
{
|
|
120
126
|
"name": "SchedulingScreen",
|
|
@@ -134,6 +140,30 @@
|
|
|
134
140
|
"tiers": [
|
|
135
141
|
"read"
|
|
136
142
|
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "MeetingContactLinks",
|
|
146
|
+
"file": "components/meeting-contact-links.tsx",
|
|
147
|
+
"purpose": "\"Open <attendee>\" buttons for a booked or recorded meeting: one per attendee who is a CRM contact; for MeetingsScreen renderDetailActions.",
|
|
148
|
+
"operations": [
|
|
149
|
+
"get_booking_appointments_bookings__booking_uid__get",
|
|
150
|
+
"get_meeting_inbox_meetings__meeting_id__get",
|
|
151
|
+
"list_contacts_contacts_get"
|
|
152
|
+
],
|
|
153
|
+
"tiers": [
|
|
154
|
+
"read"
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "RecordMeetingsTab",
|
|
159
|
+
"file": "components/record-meetings-tab.tsx",
|
|
160
|
+
"purpose": "The meetings booked with one contact (booked, rescheduled, cancelled), each opening in the meetings screen; an extraDetailTabs entry for ContactsScreen.",
|
|
161
|
+
"operations": [
|
|
162
|
+
"get_contact_meetings_contacts__contact_id__meetings_get"
|
|
163
|
+
],
|
|
164
|
+
"tiers": [
|
|
165
|
+
"read"
|
|
166
|
+
]
|
|
137
167
|
}
|
|
138
168
|
],
|
|
139
169
|
"hooks": [
|
|
@@ -216,6 +246,7 @@
|
|
|
216
246
|
],
|
|
217
247
|
"scopes": [
|
|
218
248
|
"campaigns:read",
|
|
249
|
+
"contacts:read",
|
|
219
250
|
"meetings:delete",
|
|
220
251
|
"meetings:read",
|
|
221
252
|
"meetings:run",
|
|
@@ -23,6 +23,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
23
23
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
24
|
import { useSchedules } from "../hooks/use-schedules";
|
|
25
25
|
import { WEEKDAYS, listOf, toHHMM, unwrap, type AvailabilityRule, type Schedule, type TimeWindow } from "../lib/appointments-types";
|
|
26
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
26
27
|
|
|
27
28
|
export interface AvailabilityEditorProps {
|
|
28
29
|
/** Schedule to edit; defaults to the org's default schedule. */
|
|
@@ -72,6 +73,7 @@ function WindowRow({ window: w, onChange, onRemove }: { window: TimeWindow; onCh
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
function CalendarPanel({ connectUrl }: { connectUrl: string }) {
|
|
76
|
+
const brand = useBrand();
|
|
75
77
|
const cals = useG8Query(async (signal) => listOf<CalendarRow>(await g8Get("/appointments/calendars", undefined, { signal }), "calendars"), []);
|
|
76
78
|
return (
|
|
77
79
|
<section aria-label="Calendars" className="space-y-2 rounded-lg border p-3 text-sm">
|
|
@@ -84,22 +86,26 @@ function CalendarPanel({ connectUrl }: { connectUrl: string }) {
|
|
|
84
86
|
))}
|
|
85
87
|
{!cals.loading && !cals.error && (cals.data ?? []).length === 0 ? <p className="text-muted-foreground">No calendar connected, so busy time is not checked.</p> : null}
|
|
86
88
|
<a className="underline" href={connectUrl} target="_blank" rel="noreferrer">
|
|
87
|
-
Connect or manage calendars in
|
|
89
|
+
Connect or manage calendars in {brand.linkLabel}
|
|
88
90
|
</a>
|
|
89
91
|
</section>
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export function AvailabilityEditor({ scheduleId, connectCalendarUrl
|
|
95
|
+
export function AvailabilityEditor({ scheduleId, connectCalendarUrl: connectCalendarUrlProp, onSaved, className }: AvailabilityEditorProps) {
|
|
96
|
+
const brand = useBrand();
|
|
97
|
+
const connectCalendarUrl = connectCalendarUrlProp ?? brand.appUrl;
|
|
94
98
|
const { schedules, loading: loadingSchedules, error: schedulesError } = useSchedules();
|
|
95
99
|
const [picked, setPicked] = useState<number | undefined>(scheduleId);
|
|
96
100
|
const activeId = picked ?? schedules[0]?.id;
|
|
97
101
|
const [week, setWeek] = useState<Week>({});
|
|
98
102
|
const [overrides, setOverrides] = useState<Override[]>([]);
|
|
99
103
|
const [newDate, setNewDate] = useState("");
|
|
104
|
+
/** The schedule whose hours are in the form. Save is offered only for a schedule that has loaded, never over a blank week. */
|
|
105
|
+
const [appliedFor, setAppliedFor] = useState<number | undefined>(undefined);
|
|
100
106
|
|
|
101
107
|
// Stale context: switching schedule must not show (or save!) the previous schedule's hours.
|
|
102
|
-
useEffect(() => { setWeek({}); setOverrides([]); }, [activeId]);
|
|
108
|
+
useEffect(() => { setWeek({}); setOverrides([]); setAppliedFor(undefined); }, [activeId]);
|
|
103
109
|
|
|
104
110
|
const availability = useG8Query(
|
|
105
111
|
async (signal) => unwrap<Schedule>(await g8Get(`/appointments/schedules/${activeId}/availability`, undefined, { signal })),
|
|
@@ -109,9 +115,10 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
109
115
|
useEffect(() => {
|
|
110
116
|
const data = availability.data;
|
|
111
117
|
if (!data) return;
|
|
118
|
+
setAppliedFor(activeId);
|
|
112
119
|
setWeek(toWeek(data.availability ?? []));
|
|
113
120
|
setOverrides((data.overrides ?? []).filter((o) => o.date).map((o) => ({ date: o.date as string, start: toHHMM(o.start_time), end: toHHMM(o.end_time, "17:00") })));
|
|
114
|
-
}, [availability.data]);
|
|
121
|
+
}, [availability.data, activeId]);
|
|
115
122
|
|
|
116
123
|
const save = useG8Mutation(
|
|
117
124
|
async () =>
|
|
@@ -122,10 +129,11 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
122
129
|
{ onSuccess: () => onSaved?.() },
|
|
123
130
|
);
|
|
124
131
|
|
|
132
|
+
const ready = appliedFor === activeId && !availability.error && !availability.loading;
|
|
125
133
|
const setDay = (day: number, windows: TimeWindow[]) => setWeek((w) => ({ ...w, [day]: windows }));
|
|
126
134
|
if (schedulesError) return <ApiError error={schedulesError} title="Could not load schedules" />;
|
|
127
135
|
if (loadingSchedules) return <Skeleton className="h-64 w-full" aria-label="Loading schedules" />;
|
|
128
|
-
if (activeId == null) return <EmptyState title="No schedule yet" description=
|
|
136
|
+
if (activeId == null) return <EmptyState title="No schedule yet" description={`Create a working-hours schedule in ${brand.linkLabel} (or POST /appointments/schedules), then edit it here.`} />;
|
|
129
137
|
|
|
130
138
|
return (
|
|
131
139
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
@@ -141,6 +149,8 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
141
149
|
<p className="text-sm text-muted-foreground">Times are wall-clock hours in the schedule's time zone: {schedules.find((x) => x.id === activeId)?.time_zone ?? "the schedule's zone"}.</p>
|
|
142
150
|
{availability.error ? <ApiError error={availability.error} title="Could not load availability" onRetry={() => void availability.refetch()} /> : null}
|
|
143
151
|
|
|
152
|
+
{!ready ? (availability.error ? null : <Skeleton className="h-48 w-full" aria-label="Loading availability" />) : (
|
|
153
|
+
<>
|
|
144
154
|
<div className="space-y-2">
|
|
145
155
|
{WEEKDAYS.map(({ day, label }) => {
|
|
146
156
|
const windows = week[day] ?? [];
|
|
@@ -186,6 +196,8 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
186
196
|
<Button onClick={() => void save.mutate(undefined)} loading={save.loading}>
|
|
187
197
|
Save availability
|
|
188
198
|
</Button>
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
189
201
|
<CalendarPanel connectUrl={connectCalendarUrl} />
|
|
190
202
|
</div>
|
|
191
203
|
);
|
|
@@ -98,7 +98,7 @@ export function BookingPage({ eventTypeId, daysAhead = 14, timeZone, defaultAtte
|
|
|
98
98
|
if (book.data) {
|
|
99
99
|
return (
|
|
100
100
|
<div className={`space-y-2 rounded-lg border p-4 ${className ?? ""}`} role="status">
|
|
101
|
-
<h2 className="text-base font-
|
|
101
|
+
<h2 className="text-base font-medium">You are booked</h2>
|
|
102
102
|
<p className="text-sm">
|
|
103
103
|
{eventType.data?.title ?? "Meeting"} on {formatDateTime(bookedAt ?? slot?.time, zone)}. A calendar invite was sent to {email}.
|
|
104
104
|
</p>
|
|
@@ -116,7 +116,7 @@ export function BookingPage({ eventTypeId, daysAhead = 14, timeZone, defaultAtte
|
|
|
116
116
|
return (
|
|
117
117
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
118
118
|
<header>
|
|
119
|
-
<h2 className="text-base font-
|
|
119
|
+
<h2 className="text-base font-medium">{eventType.data?.title ?? "Book a meeting"}</h2>
|
|
120
120
|
<p className="text-sm text-muted-foreground">
|
|
121
121
|
{eventType.data ? `${eventType.data.length} minutes · ` : ""}times shown in {zone}
|
|
122
122
|
</p>
|
|
@@ -116,7 +116,8 @@ export function EventTypeEditor({ eventTypeId, onSaved, onDeleted, className }:
|
|
|
116
116
|
const valid = form.title.trim().length > 0 && form.length >= 5 && (slugTouched ? form.slug.trim().length > 0 : true);
|
|
117
117
|
|
|
118
118
|
if (existing.error) return <ApiError error={existing.error} title="Could not load the event type" onRetry={() => void existing.refetch()} />;
|
|
119
|
-
|
|
119
|
+
// Also while the NEXT event type loads (the hook keeps the previous one's data meanwhile): the form is blank then, and saving it would blank the record.
|
|
120
|
+
if (eventTypeId != null && (existing.loading || !existing.data)) return <Skeleton className="h-64 w-full" aria-label="Loading event type" />;
|
|
120
121
|
|
|
121
122
|
return (
|
|
122
123
|
<form
|
|
@@ -21,6 +21,8 @@ import { g8Get } from "@/g8/lib/g8-client";
|
|
|
21
21
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
22
|
import { formatDateTime, listOf, unwrap, type Booking, type EventType } from "../lib/appointments-types";
|
|
23
23
|
import { MeetingDetail } from "./meeting-detail";
|
|
24
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
25
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
24
26
|
|
|
25
27
|
export function EventTypeOverview({ eventTypeId }: { eventTypeId: number }) {
|
|
26
28
|
const q = useG8Query(async (signal) => unwrap<EventType>(await g8Get(`/appointments/event-types/${eventTypeId}`, undefined, { signal })), [eventTypeId]);
|
|
@@ -52,8 +54,8 @@ export function EventTypeBookings({ eventTypeId, pageSize = 20 }: { eventTypeId:
|
|
|
52
54
|
<TableHeader><TableRow><TableHead>When</TableHead><TableHead>Attendees</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
|
|
53
55
|
<TableBody>
|
|
54
56
|
{q.data.map((b) => (
|
|
55
|
-
<TableRow key={b.uid} className="cursor-pointer"
|
|
56
|
-
<TableCell><
|
|
57
|
+
<TableRow key={b.uid} className="cursor-pointer" {...rowActivationProps(() => setOpen(b.uid))}>
|
|
58
|
+
<TableCell><RecordLink id={b.uid} onOpen={() => setOpen(b.uid)}>{formatDateTime(b.start_time)}</RecordLink></TableCell>
|
|
57
59
|
<TableCell className="max-w-64 truncate">{b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees"}</TableCell>
|
|
58
60
|
<TableCell><StatusPill tone={toneForStatus(b.status)} dot>{humanizeStatus(b.status)}</StatusPill></TableCell>
|
|
59
61
|
</TableRow>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MeetingContactLinks: "Open <attendee>" buttons for a booked or recorded meeting, one per attendee (at most three)
|
|
5
|
+
* who is a contact in your CRM. Use it in MeetingsScreen's `renderDetailActions`:
|
|
6
|
+
* renderDetailActions={(selection) => <MeetingContactLinks selection={selection} onOpenContact={(id) => router.push(`/contacts?id=${id}`)} />}
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getBooking GET /appointments/bookings/{uid} (read) for a booking
|
|
10
|
+
* getMeeting GET /inbox/meetings/{id} (read) for a recorded meeting
|
|
11
|
+
* 2. listContacts GET /contacts?email= (read) once per attendee, via the crm ContactLookupLink
|
|
12
|
+
* Nothing here is billable, external or destructive.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
16
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
|
+
import { ContactLookupLink } from "@/g8/crm/components/contact-lookup-link";
|
|
18
|
+
import { unwrap } from "../lib/appointments-types";
|
|
19
|
+
import type { MeetingSelection } from "./meeting-list";
|
|
20
|
+
|
|
21
|
+
interface Attendee { email?: string | null; name?: string | null }
|
|
22
|
+
|
|
23
|
+
export interface MeetingContactLinksProps {
|
|
24
|
+
selection: MeetingSelection;
|
|
25
|
+
/** Route to the contact in your contacts screen. */
|
|
26
|
+
onOpenContact: (contactId: number) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function MeetingContactLinks({ selection, onOpenContact }: MeetingContactLinksProps) {
|
|
30
|
+
const { kind, id } = selection;
|
|
31
|
+
const q = useG8Query(async (signal) => {
|
|
32
|
+
const path = kind === "booking" ? `/appointments/bookings/${encodeURIComponent(id)}` : `/inbox/meetings/${encodeURIComponent(id)}`;
|
|
33
|
+
return unwrap<{ attendees?: Attendee[] | null }>(await g8Get(path, undefined, { signal })).attendees ?? [];
|
|
34
|
+
}, [kind, id]);
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
const people = (q.data ?? []).filter((a) => {
|
|
37
|
+
const email = a.email?.trim().toLowerCase();
|
|
38
|
+
if (!email || seen.has(email)) return false;
|
|
39
|
+
seen.add(email);
|
|
40
|
+
return true;
|
|
41
|
+
}).slice(0, 3);
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
{people.map((a) => (
|
|
45
|
+
<ContactLookupLink key={a.email} email={a.email} label={`Open ${a.name || a.email}`} onOpen={onOpenContact} />
|
|
46
|
+
))}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -48,7 +48,7 @@ function RecordedDetail({ id, className }: { id: string; className?: string }) {
|
|
|
48
48
|
const m = q.data;
|
|
49
49
|
return (
|
|
50
50
|
<section className={`space-y-3 ${className ?? ""}`}>
|
|
51
|
-
<h2 className="text-base font-
|
|
51
|
+
<h2 className="text-base font-medium">{m.subject || "Untitled meeting"}</h2>
|
|
52
52
|
<dl className="grid gap-3 sm:grid-cols-2">
|
|
53
53
|
<Field label="When">{formatDateTime(m.start_time)}</Field>
|
|
54
54
|
<Field label="Attendees">{(m.attendees ?? []).map((a) => a.name || a.email).join(", ") || "Unknown"}</Field>
|
|
@@ -94,7 +94,7 @@ function BookingDetail({ uid, onChanged, className }: { uid: string; onChanged?:
|
|
|
94
94
|
|
|
95
95
|
return (
|
|
96
96
|
<section className={`space-y-4 ${className ?? ""}`}>
|
|
97
|
-
<h2 className="text-base font-
|
|
97
|
+
<h2 className="text-base font-medium">{b.title || b.event_type_title || "Meeting"}</h2>
|
|
98
98
|
<dl className="grid gap-3 sm:grid-cols-2">
|
|
99
99
|
<Field label="When">{formatDateTime(b.start_time)}</Field>
|
|
100
100
|
<Field label="Status"><span className="capitalize">{b.status}</span></Field>
|
|
@@ -18,12 +18,18 @@ import { ApiError } from "@/g8/components/api-error";
|
|
|
18
18
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
19
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
20
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
22
|
+
import { SelectFilter, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
21
23
|
import { formatDateTime, listOf, type Booking, type RecordedMeeting } from "../lib/appointments-types";
|
|
24
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
25
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
22
26
|
|
|
23
27
|
export type MeetingView = "upcoming" | "past" | "cancelled" | "recorded";
|
|
24
28
|
export type MeetingSelection = { kind: "booking"; id: string } | { kind: "meeting"; id: string };
|
|
25
29
|
|
|
26
30
|
export interface MeetingListProps {
|
|
31
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
32
|
+
linkParam?: string;
|
|
27
33
|
onSelect?: (selection: MeetingSelection) => void;
|
|
28
34
|
initialView?: MeetingView;
|
|
29
35
|
pageSize?: number;
|
|
@@ -41,8 +47,8 @@ const VIEWS: ReadonlyArray<{ value: MeetingView; label: string }> = [
|
|
|
41
47
|
{ value: "recorded", label: "Recorded" },
|
|
42
48
|
];
|
|
43
49
|
|
|
44
|
-
function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number) {
|
|
45
|
-
const base = { skip: page * take, take };
|
|
50
|
+
function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number, attendeeEmail?: string) {
|
|
51
|
+
const base = { skip: page * take, take, attendee_email: attendeeEmail || undefined };
|
|
46
52
|
if (view === "upcoming") return { ...base, after_start: now, sort_by: "start_time", sort_order: "asc" };
|
|
47
53
|
if (view === "past") return { ...base, before_end: now, sort_by: "start_time", sort_order: "desc" };
|
|
48
54
|
return { ...base, status: "cancelled", sort_by: "start_time", sort_order: "desc" };
|
|
@@ -52,36 +58,38 @@ function attendeeNames(b: Booking): string {
|
|
|
52
58
|
return b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees";
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
useEffect(() => { const t = setTimeout(() => setV(value.trim()), ms); return () => clearTimeout(t); }, [value, ms]);
|
|
58
|
-
return v;
|
|
59
|
-
}
|
|
61
|
+
/** Every filter the meetings (/inbox/meetings) and bookings APIs take besides the view tab. */
|
|
62
|
+
const MEETING_FILTERS = ["search", "participant_email", "timeframe", "has_transcript", "attendee_email"] as const;
|
|
60
63
|
|
|
61
|
-
export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
|
|
64
|
+
export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
|
|
62
65
|
const [view, setView] = useState<MeetingView>(initialView);
|
|
63
66
|
const [page, setPage] = useState(0);
|
|
64
|
-
|
|
65
|
-
const
|
|
67
|
+
// Every filter the meetings and bookings APIs take lives in the URL (`m_participant_email=...`).
|
|
68
|
+
const uf = useUrlFilters(MEETING_FILTERS, { prefix: "m_" });
|
|
69
|
+
const search = uf.draft.search;
|
|
70
|
+
const query = uf.filters.search.trim();
|
|
71
|
+
const { participant_email, timeframe, has_transcript, attendee_email } = uf.filters;
|
|
66
72
|
const now = useMemo(() => new Date().toISOString(), [view]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
73
|
|
|
68
74
|
const rows = useG8Query(
|
|
69
75
|
async (signal) => {
|
|
70
76
|
if (view === "recorded") {
|
|
71
|
-
const env = await g8Get("/inbox/meetings", { search: query || undefined, page: page + 1, page_size: pageSize }, { signal });
|
|
77
|
+
const env = await g8Get("/inbox/meetings", { search: query || undefined, participant_email: participant_email ? participant_email.split(",").map((e) => e.trim()).filter(Boolean) : undefined, timeframe: timeframe || undefined, has_transcript: has_transcript ? has_transcript === "yes" : undefined, page: page + 1, page_size: pageSize }, { signal });
|
|
72
78
|
return { kind: "meeting" as const, items: listOf<RecordedMeeting>(env), more: Boolean((env as { pagination?: { has_next?: boolean } }).pagination?.has_next) };
|
|
73
79
|
}
|
|
74
|
-
const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize), { signal });
|
|
80
|
+
const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize, attendee_email.trim()), { signal });
|
|
75
81
|
const items = listOf<Booking>(env, "bookings");
|
|
76
82
|
return { kind: "booking" as const, items, more: items.length === pageSize };
|
|
77
83
|
},
|
|
78
|
-
[view, page, query, pageSize, now, refreshKey],
|
|
84
|
+
[view, page, query, participant_email, timeframe, has_transcript, attendee_email, pageSize, now, refreshKey],
|
|
85
|
+
{ enabled: uf.ready },
|
|
79
86
|
);
|
|
80
87
|
|
|
81
88
|
const switchView = (next: MeetingView) => {
|
|
82
89
|
setView(next);
|
|
83
90
|
setPage(0);
|
|
84
91
|
};
|
|
92
|
+
useEffect(() => { setPage(0); }, [query, participant_email, timeframe, has_transcript, attendee_email]);
|
|
85
93
|
const result = rows.data;
|
|
86
94
|
|
|
87
95
|
return (
|
|
@@ -94,7 +102,19 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
|
|
|
94
102
|
</Button>
|
|
95
103
|
))}
|
|
96
104
|
</div>
|
|
97
|
-
{view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) =>
|
|
105
|
+
{view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) => uf.set("search", e.target.value)} /> : null}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
108
|
+
{view === "recorded" ? (
|
|
109
|
+
<>
|
|
110
|
+
<TextFilter label="participant email" placeholder="Participant email" type="email" value={uf.draft.participant_email} onChange={(v) => uf.set("participant_email", v)} className="w-52" />
|
|
111
|
+
<SelectFilter label="timeframe" any="Past (default)" value={uf.draft.timeframe} onChange={(v) => uf.set("timeframe", v)} options={[{ value: "upcoming", label: "Upcoming" }, { value: "all", label: "All" }]} />
|
|
112
|
+
<SelectFilter label="transcript" any="With or without transcript" value={uf.draft.has_transcript} onChange={(v) => uf.set("has_transcript", v)} options={[{ value: "yes", label: "Has a transcript" }, { value: "no", label: "No transcript" }]} />
|
|
113
|
+
</>
|
|
114
|
+
) : (
|
|
115
|
+
<TextFilter label="attendee email" placeholder="Attendee email" type="email" value={uf.draft.attendee_email} onChange={(v) => uf.set("attendee_email", v)} className="w-52" />
|
|
116
|
+
)}
|
|
117
|
+
{uf.active ? <Button size="sm" variant="ghost" onClick={uf.clear}>Clear filters</Button> : null}
|
|
98
118
|
</div>
|
|
99
119
|
|
|
100
120
|
{rows.error ? (
|
|
@@ -120,17 +140,17 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
|
|
|
120
140
|
<TableBody>
|
|
121
141
|
{result.kind === "booking"
|
|
122
142
|
? (result.items as Booking[]).map((b) => (
|
|
123
|
-
<TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer"
|
|
143
|
+
<TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "booking", id: b.uid })) : {})}>
|
|
124
144
|
<TableCell className="whitespace-nowrap">{formatDateTime(b.start_time)}</TableCell>
|
|
125
|
-
<TableCell className="font-medium">{b.title || b.event_type_title || "Meeting"}</TableCell>
|
|
145
|
+
<TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={`booking:${b.uid}`} onOpen={() => onSelect({ kind: "booking", id: b.uid })}>{b.title || b.event_type_title || "Meeting"}</RecordLink> : b.title || b.event_type_title || "Meeting"}</TableCell>
|
|
126
146
|
<TableCell className="max-w-64 truncate">{attendeeNames(b)}</TableCell>
|
|
127
147
|
<TableCell className="capitalize">{b.status}</TableCell>
|
|
128
148
|
</TableRow>
|
|
129
149
|
))
|
|
130
150
|
: (result.items as RecordedMeeting[]).map((m) => (
|
|
131
|
-
<TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer"
|
|
151
|
+
<TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "meeting", id: m.id })) : {})}>
|
|
132
152
|
<TableCell className="whitespace-nowrap">{formatDateTime(m.start_time)}</TableCell>
|
|
133
|
-
<TableCell className="font-medium">{m.subject || "Untitled meeting"}</TableCell>
|
|
153
|
+
<TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={`meeting:${m.id}`} onOpen={() => onSelect({ kind: "meeting", id: m.id })}>{m.subject || "Untitled meeting"}</RecordLink> : m.subject || "Untitled meeting"}</TableCell>
|
|
134
154
|
<TableCell className="max-w-64 truncate">{m.transcript_summary || "No summary yet"}</TableCell>
|
|
135
155
|
<TableCell className="capitalize">{m.transcript_status ?? "none"}</TableCell>
|
|
136
156
|
</TableRow>
|
|
@@ -40,6 +40,7 @@ import { listOf, type Booking } from "../lib/appointments-types";
|
|
|
40
40
|
import { BookingPage } from "./booking-page";
|
|
41
41
|
import { MeetingDetail } from "./meeting-detail";
|
|
42
42
|
import { MeetingList, type MeetingSelection } from "./meeting-list";
|
|
43
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
43
44
|
|
|
44
45
|
export interface MeetingsScreenProps {
|
|
45
46
|
/** Where a person connects a calendar (graph8 app). */
|
|
@@ -99,7 +100,9 @@ function ScheduleSheet({ open, onOpenChange, onBooked }: { open: boolean; onOpen
|
|
|
99
100
|
);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
export function MeetingsScreen({ connectCalendarUrl
|
|
103
|
+
export function MeetingsScreen({ connectCalendarUrl: connectCalendarUrlProp, renderDetailActions, extraDetailTabs, toolbarActions }: MeetingsScreenProps) {
|
|
104
|
+
const brand = useBrand();
|
|
105
|
+
const connectCalendarUrl = connectCalendarUrlProp ?? brand.appUrl;
|
|
103
106
|
const [raw, setRaw] = useUrlState("sel");
|
|
104
107
|
const [mode, setMode] = useUrlState("view");
|
|
105
108
|
const selection = parseSelection(raw);
|
|
@@ -143,10 +146,10 @@ export function MeetingsScreen({ connectCalendarUrl = "https://app.graph8.com",
|
|
|
143
146
|
expanded={mode === "full" && Boolean(selection)}
|
|
144
147
|
onExpandedChange={(full) => setMode(full ? "full" : null)}
|
|
145
148
|
detail={detail}
|
|
146
|
-
list={<MeetingList refreshKey={version} selectedId={selection?.id ?? null} onSelect={(s) => setRaw(`${s.kind}:${s.id}`)} />}
|
|
149
|
+
list={<MeetingList linkParam="sel" refreshKey={version} selectedId={selection?.id ?? null} onSelect={(s) => setRaw(`${s.kind}:${s.id}`)} />}
|
|
147
150
|
/>
|
|
148
|
-
<SectionCard title="Calendar sync" description=
|
|
149
|
-
<a className="inline-flex items-center gap-1 text-sm underline" href={connectCalendarUrl} target="_blank" rel="noreferrer">Connect a calendar in
|
|
151
|
+
<SectionCard title="Calendar sync" description={`Google and Microsoft sign-in happens in the ${brand.linkLabel} app. Once connected, booked and recorded meetings appear here.`}>
|
|
152
|
+
<a className="inline-flex items-center gap-1 text-sm underline" href={connectCalendarUrl} target="_blank" rel="noreferrer">Connect a calendar in {brand.linkLabel}<ExternalLink className="size-3.5" aria-hidden /></a>
|
|
150
153
|
</SectionCard>
|
|
151
154
|
<ScheduleSheet open={scheduling} onOpenChange={setScheduling} onBooked={changed} />
|
|
152
155
|
</ScreenLayout>
|