@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,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ContactPicker: choose one or several contacts by name instead of typing contact ids. A searchable select over the
|
|
5
|
+
* organisation's contacts; it reports the contact (id, name, hint), so a screen can show who was chosen and send the id.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /contacts?name|email&page&limit (contacts.listContacts) read - fetched only once the picker is opened
|
|
9
|
+
*
|
|
10
|
+
* Search: a first or last name (partial match); text containing "@" is looked up as a work email (exact match).
|
|
11
|
+
* Paged (usePickerPages): 25 contacts per page, "Show more" loads the next page and keeps the earlier ones; a new search
|
|
12
|
+
* starts over and an out-of-date response never lands. Locked emails and phones come back as "***" and are never shown.
|
|
13
|
+
* `multiple` keeps the panel open and shows the chosen contacts as removable chips.
|
|
14
|
+
*/
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { X } from "lucide-react";
|
|
17
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
18
|
+
import { usePickerPages } from "../hooks/use-picker-pages";
|
|
19
|
+
import { SearchPicker } from "./search-picker";
|
|
20
|
+
|
|
21
|
+
/** One selectable contact. */
|
|
22
|
+
export interface ContactOption {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
/** Job title and company, or the work email: what tells two people with the same name apart. */
|
|
26
|
+
hint?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ContactPickerProps {
|
|
30
|
+
/** Single mode: the chosen contact (controlled). */
|
|
31
|
+
value?: ContactOption | null;
|
|
32
|
+
/** Single mode: called with the contact the person chose. */
|
|
33
|
+
onChange?: (contact: ContactOption) => void;
|
|
34
|
+
/** Several contacts: tick as many as needed. Use `selected` and `onSelectedChange`. */
|
|
35
|
+
multiple?: boolean;
|
|
36
|
+
selected?: ContactOption[];
|
|
37
|
+
onSelectedChange?: (contacts: ContactOption[]) => void;
|
|
38
|
+
/** Accessible name and trigger prefix. Default "Contact". */
|
|
39
|
+
label?: string;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Id of the trigger button, so a <label htmlFor> can name it. */
|
|
43
|
+
id?: string;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Row {
|
|
48
|
+
id?: number | null;
|
|
49
|
+
first_name?: string | null;
|
|
50
|
+
last_name?: string | null;
|
|
51
|
+
job_title?: string | null;
|
|
52
|
+
company_name?: string | null;
|
|
53
|
+
work_email?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PAGE = 25;
|
|
57
|
+
const real = (v?: string | null): string => (v && !v.includes("*") ? v.trim() : "");
|
|
58
|
+
|
|
59
|
+
/** A contacts-list row as a selectable option; null without an id. */
|
|
60
|
+
export function toContactOption(r: Row): ContactOption | null {
|
|
61
|
+
if (r.id == null) return null;
|
|
62
|
+
const email = real(r.work_email);
|
|
63
|
+
const name = [real(r.first_name), real(r.last_name)].filter(Boolean).join(" ") || email || `Contact ${r.id}`;
|
|
64
|
+
const where = [real(r.job_title), real(r.company_name)].filter(Boolean).join(" at ");
|
|
65
|
+
return { id: r.id, name, hint: where || (email && email !== name ? email : undefined) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ContactPicker(props: ContactPickerProps) {
|
|
69
|
+
const { value, onChange, multiple, selected = [], onSelectedChange, label = "Contact", placeholder, disabled, id, className } = props;
|
|
70
|
+
const [open, setOpen] = useState(false);
|
|
71
|
+
const pages = usePickerPages<ContactOption>(open, async (q, page, signal) => {
|
|
72
|
+
const filter = q.includes("@") ? { email: q } : { name: q || undefined };
|
|
73
|
+
const env = await g8Get("/contacts", { ...filter, page, limit: PAGE }, { signal });
|
|
74
|
+
const rows = ((env.data ?? []) as Row[]).map(toContactOption).filter((o): o is ContactOption => o !== null);
|
|
75
|
+
const has = (env.pagination as { has_next?: boolean } | undefined)?.has_next;
|
|
76
|
+
return { rows, hasNext: has ?? rows.length >= PAGE };
|
|
77
|
+
});
|
|
78
|
+
const { rows, q } = pages;
|
|
79
|
+
const byId = new Map(rows.map((r) => [String(r.id), r]));
|
|
80
|
+
const chosenKeys = new Set((multiple ? selected : value ? [value] : []).map((c) => String(c.id)));
|
|
81
|
+
|
|
82
|
+
const toggle = (key: string) => {
|
|
83
|
+
const c = byId.get(key) ?? [...selected, ...(value ? [value] : [])].find((x) => String(x.id) === key);
|
|
84
|
+
if (!c) return;
|
|
85
|
+
if (multiple) {
|
|
86
|
+
onSelectedChange?.(chosenKeys.has(key) ? selected.filter((x) => String(x.id) !== key) : [...selected, c]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setOpen(false);
|
|
90
|
+
pages.setSearch("");
|
|
91
|
+
onChange?.(c);
|
|
92
|
+
};
|
|
93
|
+
return (
|
|
94
|
+
<div className={`flex flex-col gap-2 ${className ?? ""}`}>
|
|
95
|
+
<SearchPicker
|
|
96
|
+
label={label}
|
|
97
|
+
id={id}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
placeholder={placeholder ?? (multiple ? "Add contacts" : undefined)}
|
|
100
|
+
searchPlaceholder="Search by name or work email"
|
|
101
|
+
triggerContent={!multiple && value ? <span>{value.name}</span> : undefined}
|
|
102
|
+
options={rows.map((r) => ({ key: String(r.id), label: r.name, hint: r.hint }))}
|
|
103
|
+
selectedKeys={chosenKeys}
|
|
104
|
+
onSelect={toggle}
|
|
105
|
+
search={pages.search}
|
|
106
|
+
onSearchChange={pages.setSearch}
|
|
107
|
+
open={open}
|
|
108
|
+
onOpenChange={setOpen}
|
|
109
|
+
loading={pages.loading}
|
|
110
|
+
error={pages.error}
|
|
111
|
+
onRetry={pages.refetch}
|
|
112
|
+
hasMore={pages.hasMore}
|
|
113
|
+
loadingMore={pages.loadingMore}
|
|
114
|
+
onLoadMore={pages.loadMore}
|
|
115
|
+
emptyText={q ? "No contact matches that search." : "No contacts yet."}
|
|
116
|
+
/>
|
|
117
|
+
{multiple && selected.length > 0 ? (
|
|
118
|
+
<ul aria-label={`Chosen ${label.toLowerCase()}s`} className="flex flex-wrap gap-1">
|
|
119
|
+
{selected.map((c) => (
|
|
120
|
+
<li key={c.id} className="flex items-center gap-1 rounded-full border border-border bg-muted px-2 py-0.5 text-xs">
|
|
121
|
+
<span>{c.name}</span>
|
|
122
|
+
<button type="button" aria-label={`Remove ${c.name}`} className="text-muted-foreground hover:text-foreground" disabled={disabled} onClick={() => onSelectedChange?.(selected.filter((x) => x.id !== c.id))}>
|
|
123
|
+
<X className="size-3" aria-hidden />
|
|
124
|
+
</button>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
) : null}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MemberPicker: choose a teammate by name instead of typing a user id. A searchable select over the organisation's
|
|
5
|
+
* team; the value it reports is the user id the API operations take (a dialer rep, a record owner, a role holder).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /team-members?search&status&limit (teamMembers.listTeamMembers) read - fetched only once the picker is opened
|
|
9
|
+
*
|
|
10
|
+
* Paged: it loads 25 people and "Show more" asks for the next 25 (the search box narrows by name or email). People
|
|
11
|
+
* without a user id (a roster row that never signed in) are left out, because no operation could take them.
|
|
12
|
+
* A 403 (a key that may not read the team) shows a plain note instead of an error panel; the caller keeps its own
|
|
13
|
+
* fallback, usually the signed-in member from `useCurrentMember`.
|
|
14
|
+
*/
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
|
+
import { useDebounced } from "../lib/use-debounced";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { memberRecordsOf, toMemberOption } from "../hooks/use-current-member";
|
|
20
|
+
import type { MemberOption } from "../hooks/use-current-member";
|
|
21
|
+
import { SearchPicker } from "./search-picker";
|
|
22
|
+
|
|
23
|
+
export type { MemberOption } from "../hooks/use-current-member";
|
|
24
|
+
|
|
25
|
+
export interface MemberPickerProps {
|
|
26
|
+
/** The selected user id (controlled). */
|
|
27
|
+
value?: string | null;
|
|
28
|
+
onChange: (member: MemberOption) => void;
|
|
29
|
+
/** What to show for `value` when that person is not on the loaded page (for example the current holder of a role). */
|
|
30
|
+
selectedName?: string | null;
|
|
31
|
+
/** Accessible name and trigger prefix. Default "Team member". */
|
|
32
|
+
label?: string;
|
|
33
|
+
placeholder?: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Id of the trigger button, so a <label htmlFor> can name it. */
|
|
36
|
+
id?: string;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PAGE = 25;
|
|
41
|
+
|
|
42
|
+
export function MemberPicker({ value, onChange, selectedName, label = "Team member", placeholder, disabled, id, className }: MemberPickerProps) {
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
const [search, setSearch] = useState("");
|
|
45
|
+
const [limit, setLimit] = useState(PAGE);
|
|
46
|
+
const [chosen, setChosen] = useState<MemberOption | null>(null);
|
|
47
|
+
const q = useDebounced(search.trim());
|
|
48
|
+
const query = useG8Query(
|
|
49
|
+
async (signal) => {
|
|
50
|
+
const env = await g8Get("/team-members", { search: q || undefined, status: "active", limit, offset: 0 }, { signal });
|
|
51
|
+
const records = memberRecordsOf(env);
|
|
52
|
+
// `raw` is what the API returned, before any filtering here: "Show more" asks for more when the page was full.
|
|
53
|
+
const active = records.filter((m) => !m.status || m.status === "active");
|
|
54
|
+
return { rows: active.map(toMemberOption).filter((o): o is MemberOption => o !== null), raw: records.length };
|
|
55
|
+
},
|
|
56
|
+
[q, limit],
|
|
57
|
+
{ enabled: open },
|
|
58
|
+
);
|
|
59
|
+
const rows = query.data?.rows ?? [];
|
|
60
|
+
const status = (query.error as { status?: number } | undefined)?.status;
|
|
61
|
+
const forbidden = status === 403;
|
|
62
|
+
const shown = rows.find((m) => m.id === value) ?? (chosen?.id === value ? chosen : null);
|
|
63
|
+
const name = shown?.name ?? selectedName ?? value ?? null;
|
|
64
|
+
|
|
65
|
+
const pick = (id: string) => {
|
|
66
|
+
const member = rows.find((m) => m.id === id);
|
|
67
|
+
if (!member) return;
|
|
68
|
+
setChosen(member);
|
|
69
|
+
setOpen(false);
|
|
70
|
+
setSearch("");
|
|
71
|
+
onChange(member);
|
|
72
|
+
};
|
|
73
|
+
return (
|
|
74
|
+
<SearchPicker
|
|
75
|
+
label={label}
|
|
76
|
+
id={id}
|
|
77
|
+
className={className}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
placeholder={placeholder}
|
|
80
|
+
searchPlaceholder="Search by name or email"
|
|
81
|
+
triggerContent={name ? <span>{name}</span> : undefined}
|
|
82
|
+
options={rows.map((m) => ({ key: m.id, label: m.name, hint: m.email }))}
|
|
83
|
+
selectedKeys={new Set(value ? [value] : [])}
|
|
84
|
+
onSelect={pick}
|
|
85
|
+
search={search}
|
|
86
|
+
onSearchChange={(v) => { setSearch(v); setLimit(PAGE); }}
|
|
87
|
+
open={open}
|
|
88
|
+
onOpenChange={setOpen}
|
|
89
|
+
loading={query.loading}
|
|
90
|
+
error={forbidden ? undefined : query.error}
|
|
91
|
+
onRetry={() => void query.refetch()}
|
|
92
|
+
errorOverride={forbidden ? <p role="status" className="p-2 text-sm text-muted-foreground">This API key cannot read the team list.</p> : undefined}
|
|
93
|
+
hasMore={(query.data?.raw ?? 0) >= limit}
|
|
94
|
+
loadingMore={query.loading && rows.length > 0}
|
|
95
|
+
onLoadMore={() => setLimit((l) => l + PAGE)}
|
|
96
|
+
emptyText={q ? "Nobody matches that search." : "No team members yet."}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SearchPicker: the dropdown shell behind MemberPicker and ContactPicker. A trigger button opens a panel with a search
|
|
5
|
+
* box, a listbox of options, a "Show more" pager and the loading / empty / error states. It owns no data: the caller
|
|
6
|
+
* passes the options and the search text, so any lookup (team members, contacts, ...) reuses one accessible widget.
|
|
7
|
+
*
|
|
8
|
+
* It calls no API itself. Closing: Escape, a click outside, or (single mode) choosing an option. In `multiple` mode the
|
|
9
|
+
* panel stays open so several options can be ticked.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useRef } from "react";
|
|
12
|
+
import type { ReactNode } from "react";
|
|
13
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
14
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
|
|
17
|
+
export interface SearchPickerOption {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
/** A second muted line: a job title, an email. */
|
|
21
|
+
hint?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SearchPickerProps {
|
|
25
|
+
/** Accessible name of the whole control ("Team member", "Contact"). */
|
|
26
|
+
label: string;
|
|
27
|
+
/** What the closed trigger shows; defaults to the placeholder. */
|
|
28
|
+
triggerContent?: ReactNode;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
searchPlaceholder?: string;
|
|
31
|
+
options: SearchPickerOption[];
|
|
32
|
+
selectedKeys: ReadonlySet<string>;
|
|
33
|
+
onSelect: (key: string) => void;
|
|
34
|
+
search: string;
|
|
35
|
+
onSearchChange: (value: string) => void;
|
|
36
|
+
open: boolean;
|
|
37
|
+
onOpenChange: (open: boolean) => void;
|
|
38
|
+
loading?: boolean;
|
|
39
|
+
error?: unknown;
|
|
40
|
+
onRetry?: () => void;
|
|
41
|
+
hasMore?: boolean;
|
|
42
|
+
loadingMore?: boolean;
|
|
43
|
+
onLoadMore?: () => void;
|
|
44
|
+
/** Shown instead of the list when there is nothing to pick. */
|
|
45
|
+
emptyText?: string;
|
|
46
|
+
/** Replaces the error panel for an expected refusal (for example a 403 on the team list). */
|
|
47
|
+
errorOverride?: ReactNode;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
/** Id of the trigger button, so a <label htmlFor> can name it. */
|
|
50
|
+
id?: string;
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function SearchPicker(props: SearchPickerProps) {
|
|
55
|
+
const { label, open, onOpenChange, options, selectedKeys, loading, error, disabled, className } = props;
|
|
56
|
+
const root = useRef<HTMLDivElement>(null);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!open) return;
|
|
59
|
+
const away = (e: MouseEvent) => { if (root.current && !root.current.contains(e.target as Node)) onOpenChange(false); };
|
|
60
|
+
document.addEventListener("mousedown", away);
|
|
61
|
+
return () => document.removeEventListener("mousedown", away);
|
|
62
|
+
}, [open, onOpenChange]);
|
|
63
|
+
|
|
64
|
+
const empty = !loading && !error && options.length === 0;
|
|
65
|
+
return (
|
|
66
|
+
<div ref={root} className={`relative inline-block min-w-48 ${className ?? ""}`} onKeyDown={(e) => { if (e.key === "Escape" && open) { e.stopPropagation(); onOpenChange(false); } }}>
|
|
67
|
+
<Button id={props.id} type="button" variant="outline" size="sm" className="w-full justify-between" disabled={disabled} aria-label={label} aria-haspopup="listbox" aria-expanded={open} onClick={() => onOpenChange(!open)}>
|
|
68
|
+
<span className="truncate">{props.triggerContent ?? <span className="text-muted-foreground">{props.placeholder ?? `Choose ${label.toLowerCase()}`}</span>}</span>
|
|
69
|
+
<ChevronDown className="ml-2 size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
70
|
+
</Button>
|
|
71
|
+
{open ? (
|
|
72
|
+
<div className="absolute left-0 z-50 mt-1 flex w-72 max-w-[90vw] flex-col gap-2 rounded-md border border-border bg-background p-2 shadow-md">
|
|
73
|
+
<Input autoFocus aria-label={`Search ${label.toLowerCase()}`} placeholder={props.searchPlaceholder ?? "Search"} value={props.search} onChange={(e) => props.onSearchChange(e.target.value)} />
|
|
74
|
+
{props.errorOverride ?? (error ? <ApiError error={error} title={`Could not load ${label.toLowerCase()} options`} onRetry={props.onRetry} /> : null)}
|
|
75
|
+
<ul role="listbox" aria-label={label} className="max-h-56 overflow-y-auto">
|
|
76
|
+
{loading && options.length === 0 ? <li className="p-2 text-sm text-muted-foreground">Loading...</li> : null}
|
|
77
|
+
{empty ? <li className="p-2 text-sm text-muted-foreground">{props.emptyText ?? "Nothing matches."}</li> : null}
|
|
78
|
+
{options.map((o) => {
|
|
79
|
+
const on = selectedKeys.has(o.key);
|
|
80
|
+
return (
|
|
81
|
+
<li key={o.key} role="none">
|
|
82
|
+
<button type="button" role="option" aria-selected={on} className={`flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent ${on ? "bg-accent font-medium" : ""}`} onClick={() => props.onSelect(o.key)}>
|
|
83
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
84
|
+
<span className="truncate">{o.label}</span>
|
|
85
|
+
{o.hint ? <span className="truncate text-xs text-muted-foreground">{o.hint}</span> : null}
|
|
86
|
+
</span>
|
|
87
|
+
{on ? <Check className="size-4 shrink-0" aria-hidden /> : null}
|
|
88
|
+
</button>
|
|
89
|
+
</li>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</ul>
|
|
93
|
+
{props.hasMore ? <Button type="button" variant="ghost" size="sm" loading={props.loadingMore} onClick={props.onLoadMore}>Show more</Button> : null}
|
|
94
|
+
</div>
|
|
95
|
+
) : null}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useCurrentMember: the signed-in team member, so no screen has to ask a person for their own user id.
|
|
5
|
+
*
|
|
6
|
+
* It is the ONE place that resolves "who am I" for a PERSONAL API key: the dialer's rep, a record owner default and
|
|
7
|
+
* CallContactButton all read it, so they cannot drift apart. An organisation key has no person: the API answers 403
|
|
8
|
+
* and `member` stays null (`forbidden: true`); callers then offer a MemberPicker instead of a text box.
|
|
9
|
+
*
|
|
10
|
+
* SDK operations, in order:
|
|
11
|
+
* 1. GET /team-members/me (teamMembers.getMyTeamMember) read - the roster record of the person who minted the key
|
|
12
|
+
*
|
|
13
|
+
* `userId` is the identity-provider user id (`propelauth_user_id`): the id the dialer's softphone credential, session
|
|
14
|
+
* drafts and record roles take. `email` is the matching rep email; both describe the same person.
|
|
15
|
+
*/
|
|
16
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
17
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
18
|
+
|
|
19
|
+
/** A roster record as the team-members endpoints answer it (the API may return more). */
|
|
20
|
+
export interface TeamMemberRecord {
|
|
21
|
+
id?: string | null;
|
|
22
|
+
name?: string | null;
|
|
23
|
+
first_name?: string | null;
|
|
24
|
+
last_name?: string | null;
|
|
25
|
+
email?: string | null;
|
|
26
|
+
propelauth_user_id?: string | null;
|
|
27
|
+
status?: string | null;
|
|
28
|
+
role?: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One selectable person. `id` is the identity-provider user id, the value every id-taking operation wants. */
|
|
32
|
+
export interface MemberOption {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
email?: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The API envelope's `data`, or the value itself when it was already unwrapped. */
|
|
39
|
+
export function unwrapData<T>(env: unknown): T {
|
|
40
|
+
const o = env as { data?: unknown } | null | undefined;
|
|
41
|
+
return (o && typeof o === "object" && "data" in o ? o.data : env) as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A roster record as a selectable person; null when it has no user id (it cannot be a rep or an owner). */
|
|
45
|
+
export function toMemberOption(m: TeamMemberRecord | null | undefined): MemberOption | null {
|
|
46
|
+
const id = m?.propelauth_user_id;
|
|
47
|
+
if (!m || !id) return null;
|
|
48
|
+
const full = [m.first_name, m.last_name].filter(Boolean).join(" ");
|
|
49
|
+
return { id, name: m.name || full || m.email || id, email: m.email };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The list endpoints answer a bare array, or an object with `items` / `team_members`. */
|
|
53
|
+
export function memberRecordsOf(env: unknown): TeamMemberRecord[] {
|
|
54
|
+
const inner = unwrapData<unknown>(env);
|
|
55
|
+
if (Array.isArray(inner)) return inner as TeamMemberRecord[];
|
|
56
|
+
const o = inner as { items?: unknown; team_members?: unknown } | null;
|
|
57
|
+
const list = o?.items ?? o?.team_members;
|
|
58
|
+
return Array.isArray(list) ? (list as TeamMemberRecord[]) : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The signed-in member's record, or null. A 403 (organisation key) and a 404 both read as "no person". */
|
|
62
|
+
export async function fetchCurrentMember(signal?: AbortSignal): Promise<TeamMemberRecord | null> {
|
|
63
|
+
return unwrapData<TeamMemberRecord | null>(await g8Get("/team-members/me", undefined, { signal })) ?? null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface UseCurrentMemberOptions {
|
|
67
|
+
/** Set false to hold the request (for example when the caller already passed a `userId`). Default true. */
|
|
68
|
+
enabled?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface CurrentMember {
|
|
72
|
+
/** The signed-in person as a selectable option; null for an organisation key, while loading, or on failure. */
|
|
73
|
+
member: MemberOption | null;
|
|
74
|
+
/** `propelauth_user_id`: what the dialer, softphone and role assignments take. */
|
|
75
|
+
userId?: string;
|
|
76
|
+
email?: string;
|
|
77
|
+
name?: string;
|
|
78
|
+
/** The raw roster record (role, status ...), for callers that need more than the id. */
|
|
79
|
+
record: TeamMemberRecord | null;
|
|
80
|
+
loading: boolean;
|
|
81
|
+
/** The key is an organisation key: it has no person, so ask with a MemberPicker. */
|
|
82
|
+
forbidden: boolean;
|
|
83
|
+
/** Any failure other than the expected 403/404 of an organisation key. */
|
|
84
|
+
error: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useCurrentMember(options: UseCurrentMemberOptions = {}): CurrentMember {
|
|
88
|
+
const enabled = options.enabled ?? true;
|
|
89
|
+
const q = useG8Query((signal) => fetchCurrentMember(signal), [], { enabled });
|
|
90
|
+
const status = (q.error as { status?: number } | undefined)?.status;
|
|
91
|
+
const refusal = status === 403 || status === 404;
|
|
92
|
+
const record = enabled ? (q.data ?? null) : null;
|
|
93
|
+
const member = toMemberOption(record);
|
|
94
|
+
return {
|
|
95
|
+
member,
|
|
96
|
+
userId: member?.id,
|
|
97
|
+
email: record?.email ?? undefined,
|
|
98
|
+
name: member?.name,
|
|
99
|
+
record,
|
|
100
|
+
loading: enabled && q.loading && !q.data,
|
|
101
|
+
forbidden: enabled && status === 403,
|
|
102
|
+
error: enabled && q.error && !refusal ? q.error : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* usePickerPages: the paging + search engine behind ContactPicker and CompanyPicker.
|
|
5
|
+
*
|
|
6
|
+
* Page-number lists (`?page&limit`, `pagination.has_next`) with a search box: the text is debounced, a new search
|
|
7
|
+
* starts over from page 1, "load more" appends the next page to the ones already shown, and a response for an
|
|
8
|
+
* earlier search or page is dropped (latest wins). Nothing is fetched until `enabled` (the picker is open).
|
|
9
|
+
*
|
|
10
|
+
* It calls no operation itself: `fetchPage` does, so the caller's component declares the operation.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useState } from "react";
|
|
13
|
+
import { useDebounced } from "../lib/use-debounced";
|
|
14
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
15
|
+
|
|
16
|
+
export interface PickerPage<T> {
|
|
17
|
+
rows: T[];
|
|
18
|
+
hasNext: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UsePickerPages<T> {
|
|
22
|
+
search: string;
|
|
23
|
+
setSearch: (value: string) => void;
|
|
24
|
+
/** The debounced, trimmed search text the loaded rows belong to. */
|
|
25
|
+
q: string;
|
|
26
|
+
rows: T[];
|
|
27
|
+
hasMore: boolean;
|
|
28
|
+
/** The first page of this search is loading and nothing is shown yet. */
|
|
29
|
+
loading: boolean;
|
|
30
|
+
/** A following page is loading. */
|
|
31
|
+
loadingMore: boolean;
|
|
32
|
+
error: unknown;
|
|
33
|
+
refetch: () => void;
|
|
34
|
+
loadMore: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Loaded<T> extends PickerPage<T> { q: string; page: number }
|
|
38
|
+
|
|
39
|
+
export function usePickerPages<T>(
|
|
40
|
+
enabled: boolean,
|
|
41
|
+
fetchPage: (q: string, page: number, signal: AbortSignal) => Promise<PickerPage<T>>,
|
|
42
|
+
): UsePickerPages<T> {
|
|
43
|
+
const [search, setSearchText] = useState("");
|
|
44
|
+
const [page, setPage] = useState({ q: "", n: 1 });
|
|
45
|
+
const [store, setStore] = useState<{ q: string | null; pages: T[][]; hasNext: boolean }>({ q: null, pages: [], hasNext: false });
|
|
46
|
+
const q = useDebounced(search.trim());
|
|
47
|
+
const n = page.q === q ? page.n : 1;
|
|
48
|
+
const query = useG8Query(
|
|
49
|
+
async (signal): Promise<Loaded<T>> => ({ ...(await fetchPage(q, n, signal)), q, page: n }),
|
|
50
|
+
[q, n],
|
|
51
|
+
{ enabled },
|
|
52
|
+
);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const d = query.data;
|
|
55
|
+
if (!d || d.q !== q || d.page !== n) return; // a response for an earlier search or page
|
|
56
|
+
setStore((s) => ({ q, pages: [...(s.q === q ? s.pages.slice(0, n - 1) : []), d.rows], hasNext: d.hasNext }));
|
|
57
|
+
}, [query.data, q, n]);
|
|
58
|
+
|
|
59
|
+
const current = store.q === q;
|
|
60
|
+
const rows = current ? store.pages.flat() : [];
|
|
61
|
+
return {
|
|
62
|
+
search,
|
|
63
|
+
setSearch: (value) => { setSearchText(value); setPage({ q: value.trim(), n: 1 }); },
|
|
64
|
+
q,
|
|
65
|
+
rows,
|
|
66
|
+
hasMore: current && store.hasNext,
|
|
67
|
+
loading: enabled && (query.loading || !current) && rows.length === 0,
|
|
68
|
+
loadingMore: query.loading && rows.length > 0,
|
|
69
|
+
error: query.error,
|
|
70
|
+
refetch: () => { void query.refetch(); },
|
|
71
|
+
loadMore: () => setPage({ q, n: n + 1 }),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/** `value`, but only after it has stopped changing for `ms`. The search box of every picker reads through this. */
|
|
6
|
+
export function useDebounced<T>(value: T, ms = 250): T {
|
|
7
|
+
const [v, setV] = useState(value);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const t = setTimeout(() => setV(value), ms);
|
|
10
|
+
return () => clearTimeout(t);
|
|
11
|
+
}, [value, ms]);
|
|
12
|
+
return v;
|
|
13
|
+
}
|
package/blocks/quotes/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Install with `npx @graph8/sdk add quotes`.
|
|
|
10
10
|
| `ProductPicker` | Quotable products (Stripe + manual) and their prices; returns a ready line item. |
|
|
11
11
|
| `QuoteViewer` | One quote with totals and status-based actions: send/resend, edit as draft, duplicate, delete. |
|
|
12
12
|
| `SignatureStatus` | Envelope state, signers, activity, reminder. |
|
|
13
|
+
| `QuoteLifecycleActions` | PDF download, archive and unarchive, and for a sent quote expire, revoke and mark accepted. Each risky action is confirmed. |
|
|
14
|
+
| `CreateQuoteFromDeal` | A "Create quote from this deal" button (builder in a side sheet, then the saved quote). Pass it to `DealsScreen`'s `renderQuoteActions`. |
|
|
13
15
|
|
|
14
16
|
## Screens
|
|
15
17
|
|
|
@@ -19,6 +21,12 @@ Install with `npx @graph8/sdk add quotes`.
|
|
|
19
21
|
- Open a quote (deep link `?id=<quote id>`): line items, totals, signer, signature progress, with the actions its status
|
|
20
22
|
allows. Send, resend, signature reminder and edit-as-draft are external and confirmed; delete is destructive and
|
|
21
23
|
confirmed; duplicate and saving a draft email nobody.
|
|
24
|
+
- **Lifecycle** (under the quote header): Download PDF (opens the link the API returns, or saves base64 content), Archive
|
|
25
|
+
and Unarchive (the list has "Include archived"), and for a sent or viewed quote **Expire** and **Revoke** (both final,
|
|
26
|
+
destructive confirms) and **Mark accepted**. Mark accepted raises an invoice with your billing provider, so it is an
|
|
27
|
+
external confirm that also requires an evidence type (signed offline, customer email, verbal authorization, payment
|
|
28
|
+
received) and a note. Use it only when the customer paid or signed outside the product.
|
|
29
|
+
- The Deal tab of a quote offers "Create another quote" for the same deal.
|
|
22
30
|
- **New quote** asks for the deal first (search by name, skipped when `dealId` is given because graph8 attaches every
|
|
23
31
|
quote to a deal), then opens the builder (details, signer, line items from the quotable products, live totals).
|
|
24
32
|
- Edit a draft from the viewer in a side sheet.
|
|
@@ -94,3 +102,8 @@ and **delete** (destructive). From code, never call `sendQuote` without the pers
|
|
|
94
102
|
- `recipes/create-and-send-quote.ts`: Create a quote from quotable products and send it for signature
|
|
95
103
|
- `recipes/track-quote-signature.ts`: Wait for a quote to be signed (or declined)
|
|
96
104
|
- `recipes/revise-sent-quote.ts`: Change a quote that was already sent
|
|
105
|
+
|
|
106
|
+
## Cross-block glue
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
- `RecordQuotesTab` (`components/record-quotes-tab.tsx`): the quotes tied to a contact (as signer) or a company (`getContactQuotes` / `getCompanyQuotes`, unpaged), each opening `/quotes?id=<id>`. `extraDetailTabs` entry for ContactsScreen and CompaniesScreen.
|
package/blocks/quotes/block.json
CHANGED
|
@@ -8,25 +8,35 @@
|
|
|
8
8
|
"draft builder with live totals and signer search",
|
|
9
9
|
"quotable product picker (Stripe and manual products)",
|
|
10
10
|
"send, resend and edit-as-draft behind confirms",
|
|
11
|
-
"e-signature status with reminder"
|
|
11
|
+
"e-signature status with reminder",
|
|
12
|
+
"quote lifecycle: PDF download, archive and unarchive, expire, revoke and mark accepted",
|
|
13
|
+
"create a quote from a deal anywhere with one button"
|
|
12
14
|
],
|
|
13
15
|
"dependsOn": [
|
|
14
16
|
"deals"
|
|
15
17
|
],
|
|
16
18
|
"operations": [
|
|
17
|
-
"
|
|
18
|
-
"get_quote_quotes__quote_id__get",
|
|
19
|
-
"search_quote_contacts_quote_contacts_get",
|
|
19
|
+
"archive_quote_quotes__quote_id__archive_post",
|
|
20
20
|
"create_quote_quotes_post",
|
|
21
|
-
"update_quote_quotes__quote_id__patch",
|
|
22
|
-
"list_quotable_products_quotable_products_get",
|
|
23
|
-
"send_quote_quotes__quote_id__send_post",
|
|
24
|
-
"edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
|
|
25
|
-
"duplicate_quote_quotes__quote_id__duplicate_post",
|
|
26
21
|
"delete_quote_quotes__quote_id__delete",
|
|
27
|
-
"
|
|
22
|
+
"duplicate_quote_quotes__quote_id__duplicate_post",
|
|
23
|
+
"edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
|
|
24
|
+
"expire_quote_quotes__quote_id__expire_post",
|
|
25
|
+
"get_company_quotes_companies__company_id__quotes_get",
|
|
26
|
+
"get_contact_quotes_contacts__contact_id__quotes_get",
|
|
27
|
+
"get_deal_deals__deal_id__get",
|
|
28
|
+
"get_quote_pdf_quotes__quote_id__pdf_get",
|
|
29
|
+
"get_quote_quotes__quote_id__get",
|
|
28
30
|
"list_deals_deals_get",
|
|
29
|
-
"
|
|
31
|
+
"list_quotable_products_quotable_products_get",
|
|
32
|
+
"list_quotes_quotes_get",
|
|
33
|
+
"mark_quote_accepted_quotes__quote_id__mark_accepted_post",
|
|
34
|
+
"resend_envelope_envelopes__envelope_id__resend_post",
|
|
35
|
+
"revoke_quote_quotes__quote_id__revoke_post",
|
|
36
|
+
"search_quote_contacts_quote_contacts_get",
|
|
37
|
+
"send_quote_quotes__quote_id__send_post",
|
|
38
|
+
"unarchive_quote_quotes__quote_id__unarchive_post",
|
|
39
|
+
"update_quote_quotes__quote_id__patch"
|
|
30
40
|
],
|
|
31
41
|
"components": [
|
|
32
42
|
{
|
|
@@ -100,8 +110,13 @@
|
|
|
100
110
|
"name": "QuotesScreen",
|
|
101
111
|
"file": "components/quotes-screen.tsx",
|
|
102
112
|
"purpose": "Finished quotes product: status numbers, list, viewer with send and signature tracking, deal-first builder and draft editing.",
|
|
103
|
-
"operations": [
|
|
104
|
-
|
|
113
|
+
"operations": [
|
|
114
|
+
"list_deals_deals_get",
|
|
115
|
+
"list_quotes_quotes_get"
|
|
116
|
+
],
|
|
117
|
+
"tiers": [
|
|
118
|
+
"read"
|
|
119
|
+
]
|
|
105
120
|
},
|
|
106
121
|
{
|
|
107
122
|
"name": "ProductCatalog",
|
|
@@ -115,6 +130,43 @@
|
|
|
115
130
|
"tiers": [
|
|
116
131
|
"read"
|
|
117
132
|
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "QuoteLifecycleActions",
|
|
136
|
+
"file": "components/quote-lifecycle-actions.tsx",
|
|
137
|
+
"purpose": "After a quote exists: download the PDF, archive or unarchive, and for a sent quote expire, revoke or mark accepted (evidence required). Each risky action is confirmed.",
|
|
138
|
+
"operations": [
|
|
139
|
+
"archive_quote_quotes__quote_id__archive_post",
|
|
140
|
+
"expire_quote_quotes__quote_id__expire_post",
|
|
141
|
+
"get_quote_pdf_quotes__quote_id__pdf_get",
|
|
142
|
+
"mark_quote_accepted_quotes__quote_id__mark_accepted_post",
|
|
143
|
+
"revoke_quote_quotes__quote_id__revoke_post",
|
|
144
|
+
"unarchive_quote_quotes__quote_id__unarchive_post"
|
|
145
|
+
],
|
|
146
|
+
"tiers": [
|
|
147
|
+
"read",
|
|
148
|
+
"external",
|
|
149
|
+
"destructive"
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "CreateQuoteFromDeal",
|
|
154
|
+
"file": "components/create-quote-from-deal.tsx",
|
|
155
|
+
"purpose": "A 'Create quote from this deal' button: builder in a side sheet, then the saved quote with its actions. Pass it to the deals screen's renderQuoteActions.",
|
|
156
|
+
"operations": [],
|
|
157
|
+
"tiers": []
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "RecordQuotesTab",
|
|
161
|
+
"file": "components/record-quotes-tab.tsx",
|
|
162
|
+
"purpose": "The quotes tied to one contact (as signer) or one company, each opening in the quotes screen; an extraDetailTabs entry for ContactsScreen and CompaniesScreen.",
|
|
163
|
+
"operations": [
|
|
164
|
+
"get_contact_quotes_contacts__contact_id__quotes_get",
|
|
165
|
+
"get_company_quotes_companies__company_id__quotes_get"
|
|
166
|
+
],
|
|
167
|
+
"tiers": [
|
|
168
|
+
"read"
|
|
169
|
+
]
|
|
118
170
|
}
|
|
119
171
|
],
|
|
120
172
|
"hooks": [
|