@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,280 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Connect graph8 (local preview only): the first-run screen of a created app when the server has no graph8 API
|
|
5
|
+
* key yet, and the place to replace or remove the key later.
|
|
6
|
+
*
|
|
7
|
+
* Calls this app's own `/api/g8-connect` route (g8/server/connect.ts), never graph8 directly:
|
|
8
|
+
* GET status: is a key configured, where from (server environment or the pasted file), whose is it
|
|
9
|
+
* POST { key }: the server checks the key with a free read (GET /me), then stores it in .g8/local-key
|
|
10
|
+
* DELETE remove the stored key
|
|
11
|
+
* No billable, external or destructive graph8 operation (removing the local file only forgets the key here).
|
|
12
|
+
* The key goes from the input to the server once and is never kept in browser state after submit, storage or URL.
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* - ConnectGraph8Panel: the full-page panel (no required props).
|
|
16
|
+
* - G8ConnectGate: wraps the app; in `next dev` it shows the screen until a key is connected, plus a small
|
|
17
|
+
* "Local preview" button to manage the key. In a production build it renders its children and nothing else.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useCallback, useEffect, useId, useState, type FormEvent, type ReactNode } from "react";
|
|
21
|
+
import { CheckCircle2, ExternalLink, KeyRound, Loader2, X } from "lucide-react";
|
|
22
|
+
import { Button, Input, cn } from "@graph8/sdk/ui";
|
|
23
|
+
import { DEFAULT_BRAND, brandUrl, useBrand } from "../hooks/use-brand";
|
|
24
|
+
|
|
25
|
+
export const CONNECT_PATH = "/api/g8-connect";
|
|
26
|
+
/** Where API keys are managed, on the platform's web app (white-label: see hooks/use-brand). */
|
|
27
|
+
const KEYS_PATH = "/studio/settings?tab=api";
|
|
28
|
+
|
|
29
|
+
export interface ConnectStatus {
|
|
30
|
+
localPreview: boolean;
|
|
31
|
+
connected: boolean;
|
|
32
|
+
source: "env" | "file" | null;
|
|
33
|
+
keyHint?: string;
|
|
34
|
+
keyFile?: string;
|
|
35
|
+
account?: { name?: string; workspace?: string; scopes?: string[] };
|
|
36
|
+
problem?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readJson(res: Response): Promise<Record<string, unknown>> {
|
|
40
|
+
return (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** GET the status. Null when the route does not exist or refuses (production, not this machine). */
|
|
44
|
+
export async function fetchConnectStatus(): Promise<ConnectStatus | null> {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(CONNECT_PATH, { headers: { accept: "application/json" }, cache: "no-store" });
|
|
47
|
+
if (!res.ok) return null;
|
|
48
|
+
const body = await readJson(res);
|
|
49
|
+
return body.localPreview === true ? (body as unknown as ConnectStatus) : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function accountLine(s: ConnectStatus, platform = DEFAULT_BRAND.linkLabel): string {
|
|
56
|
+
const a = s.account ?? {};
|
|
57
|
+
return [a.name, a.workspace].filter(Boolean).join(" at ") || `a ${platform} key`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ConnectGraph8PanelProps {
|
|
61
|
+
/** Status from a previous GET (the gate passes it); fetched on mount when absent. */
|
|
62
|
+
status?: ConnectStatus | null;
|
|
63
|
+
/** Called after a key is stored or removed, with the new status. */
|
|
64
|
+
onChange?: (status: ConnectStatus) => void;
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Paste, check and store the server's graph8 API key; replace or remove it. */
|
|
69
|
+
export function ConnectGraph8Panel({ status: initial, onChange, className }: ConnectGraph8PanelProps) {
|
|
70
|
+
const [status, setStatus] = useState<ConnectStatus | null | undefined>(initial);
|
|
71
|
+
const [key, setKey] = useState("");
|
|
72
|
+
const [busy, setBusy] = useState<"save" | "remove" | null>(null);
|
|
73
|
+
const [error, setError] = useState<string | null>(null);
|
|
74
|
+
const [replacing, setReplacing] = useState(false);
|
|
75
|
+
const inputId = useId();
|
|
76
|
+
const brand = useBrand();
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (initial === undefined) void fetchConnectStatus().then(setStatus);
|
|
80
|
+
}, [initial]);
|
|
81
|
+
|
|
82
|
+
const apply = (next: ConnectStatus) => {
|
|
83
|
+
setStatus(next);
|
|
84
|
+
setReplacing(false);
|
|
85
|
+
onChange?.(next);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const submit = async (e: FormEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
if (!key.trim() || busy) return;
|
|
91
|
+
setBusy("save");
|
|
92
|
+
setError(null);
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(CONNECT_PATH, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ key: key.trim() }) });
|
|
95
|
+
const body = await readJson(res);
|
|
96
|
+
setKey("");
|
|
97
|
+
if (!res.ok) setError(typeof body.message === "string" ? body.message : `Could not save the key (HTTP ${res.status}).`);
|
|
98
|
+
else apply(body as unknown as ConnectStatus);
|
|
99
|
+
} catch {
|
|
100
|
+
setError("Could not reach this app's server. Is `npm run dev` still running?");
|
|
101
|
+
} finally {
|
|
102
|
+
setBusy(null);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const remove = async () => {
|
|
107
|
+
setBusy("remove");
|
|
108
|
+
setError(null);
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(CONNECT_PATH, { method: "DELETE" });
|
|
111
|
+
const body = await readJson(res);
|
|
112
|
+
if (!res.ok) setError(typeof body.message === "string" ? body.message : `Could not remove the key (HTTP ${res.status}).`);
|
|
113
|
+
else apply(body as unknown as ConnectStatus);
|
|
114
|
+
} catch {
|
|
115
|
+
setError("Could not reach this app's server. Is `npm run dev` still running?");
|
|
116
|
+
} finally {
|
|
117
|
+
setBusy(null);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const connected = Boolean(status?.connected);
|
|
122
|
+
const fromEnv = status?.source === "env";
|
|
123
|
+
const showForm = !fromEnv && (!status?.source || replacing);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<section aria-labelledby={`${inputId}-title`} className={cn("mx-auto flex w-full max-w-md flex-col gap-5 px-4 py-14", className)}>
|
|
127
|
+
<div className="flex flex-col gap-2">
|
|
128
|
+
<div aria-hidden className="flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground">
|
|
129
|
+
<KeyRound className="size-5" strokeWidth={1.5} />
|
|
130
|
+
</div>
|
|
131
|
+
<h1 id={`${inputId}-title`} className="text-lg font-medium text-foreground">
|
|
132
|
+
{connected ? `Connected to ${brand.linkLabel}` : `Connect ${brand.linkLabel}`}
|
|
133
|
+
</h1>
|
|
134
|
+
<p className="text-sm leading-5 text-text-color-muted">
|
|
135
|
+
This app reads and writes your {brand.linkLabel} data through its own server. Paste an API key once: the server checks
|
|
136
|
+
it and keeps it in <code className="text-xs">{status?.keyFile ?? ".g8/local-key"}</code> on this computer. The
|
|
137
|
+
browser never stores it.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{status?.source ? (
|
|
142
|
+
<div className="flex items-start gap-3 rounded-lg border border-border p-3 text-sm">
|
|
143
|
+
{connected ? (
|
|
144
|
+
<CheckCircle2 aria-hidden className="mt-0.5 size-4 shrink-0 text-positive-foreground" strokeWidth={1.5} />
|
|
145
|
+
) : (
|
|
146
|
+
<X aria-hidden className="mt-0.5 size-4 shrink-0 text-error-foreground" strokeWidth={1.5} />
|
|
147
|
+
)}
|
|
148
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
149
|
+
<span className="text-foreground">{connected ? accountLine(status, brand.linkLabel) : "The key no longer works"}</span>
|
|
150
|
+
<span className="text-xs text-text-color-muted">
|
|
151
|
+
Key {status.keyHint ?? ""} from {fromEnv ? "the server environment (.env.local)" : status.keyFile ?? ".g8/local-key"}
|
|
152
|
+
{status.problem ? `. ${status.problem}` : ""}
|
|
153
|
+
</span>
|
|
154
|
+
{fromEnv ? <span className="text-xs text-text-color-muted">To change it, edit .env.local and restart the dev server.</span> : null}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
) : null}
|
|
158
|
+
|
|
159
|
+
{showForm ? (
|
|
160
|
+
<form onSubmit={submit} className="flex flex-col gap-2">
|
|
161
|
+
<label htmlFor={inputId} className="text-sm font-medium text-foreground">
|
|
162
|
+
{brand.linkLabel} API key
|
|
163
|
+
</label>
|
|
164
|
+
<Input
|
|
165
|
+
id={inputId}
|
|
166
|
+
type="password"
|
|
167
|
+
autoComplete="off"
|
|
168
|
+
spellCheck={false}
|
|
169
|
+
value={key}
|
|
170
|
+
onChange={(e) => setKey(e.target.value)}
|
|
171
|
+
placeholder="Paste the key"
|
|
172
|
+
aria-invalid={error ? true : undefined}
|
|
173
|
+
aria-describedby={error ? `${inputId}-error` : undefined}
|
|
174
|
+
/>
|
|
175
|
+
{error ? (
|
|
176
|
+
<p id={`${inputId}-error`} role="alert" className="text-xs text-error-foreground">
|
|
177
|
+
{error}
|
|
178
|
+
</p>
|
|
179
|
+
) : null}
|
|
180
|
+
<div className="flex items-center gap-2 pt-1">
|
|
181
|
+
<Button type="submit" disabled={!key.trim() || busy !== null}>
|
|
182
|
+
{busy === "save" ? <Loader2 aria-hidden className="size-4 animate-spin" /> : null}
|
|
183
|
+
{busy === "save" ? "Checking" : "Connect"}
|
|
184
|
+
</Button>
|
|
185
|
+
{replacing ? (
|
|
186
|
+
<Button type="button" variant="ghost" onClick={() => setReplacing(false)}>
|
|
187
|
+
Cancel
|
|
188
|
+
</Button>
|
|
189
|
+
) : null}
|
|
190
|
+
</div>
|
|
191
|
+
</form>
|
|
192
|
+
) : status?.source === "file" ? (
|
|
193
|
+
<div className="flex flex-col gap-2">
|
|
194
|
+
<div className="flex items-center gap-2">
|
|
195
|
+
<Button type="button" variant="outline" onClick={() => setReplacing(true)} disabled={busy !== null}>
|
|
196
|
+
Replace key
|
|
197
|
+
</Button>
|
|
198
|
+
<Button type="button" variant="ghost" onClick={() => void remove()} disabled={busy !== null}>
|
|
199
|
+
{busy === "remove" ? <Loader2 aria-hidden className="size-4 animate-spin" /> : null}
|
|
200
|
+
Remove key
|
|
201
|
+
</Button>
|
|
202
|
+
</div>
|
|
203
|
+
{error ? (
|
|
204
|
+
<p role="alert" className="text-xs text-error-foreground">
|
|
205
|
+
{error}
|
|
206
|
+
</p>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
) : null}
|
|
210
|
+
|
|
211
|
+
<a href={brandUrl(brand, KEYS_PATH)} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-sm text-foreground underline-offset-4 hover:underline">
|
|
212
|
+
Get a key in {brand.linkLabel} (Settings, API)
|
|
213
|
+
<ExternalLink aria-hidden className="size-3.5" strokeWidth={1.5} />
|
|
214
|
+
</a>
|
|
215
|
+
<p className="text-xs leading-5 text-text-color-muted">
|
|
216
|
+
Local preview: this screen and the data routes only answer requests from this computer while you run the dev
|
|
217
|
+
server. Before you deploy, set <code>authorize</code> in app/api/g8/[...path]/route.ts and the key in your host's
|
|
218
|
+
environment; <code>npx @graph8/sdk doctor --production</code> checks both.
|
|
219
|
+
</p>
|
|
220
|
+
</section>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface G8ConnectGateProps {
|
|
225
|
+
children: ReactNode;
|
|
226
|
+
/** Hide the small "Local preview" button once connected. Default false. */
|
|
227
|
+
hideManageButton?: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* In `next dev`: shows ConnectGraph8Panel instead of the app until a key is connected, and a small button to
|
|
232
|
+
* manage the key afterwards. In a production build (and when /api/g8-connect is missing or refuses) it is
|
|
233
|
+
* exactly its children: no request, no extra markup.
|
|
234
|
+
*/
|
|
235
|
+
export function G8ConnectGate({ children, hideManageButton = false }: G8ConnectGateProps) {
|
|
236
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
237
|
+
const [status, setStatus] = useState<ConnectStatus | null | undefined>(dev ? undefined : null);
|
|
238
|
+
const [managing, setManaging] = useState(false);
|
|
239
|
+
const brand = useBrand();
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (dev) void fetchConnectStatus().then(setStatus);
|
|
243
|
+
}, [dev]);
|
|
244
|
+
|
|
245
|
+
const changed = useCallback((next: ConnectStatus) => {
|
|
246
|
+
setStatus(next);
|
|
247
|
+
// Every screen loaded its data with the old (or no) key: start them again.
|
|
248
|
+
if (next.connected) window.location.reload();
|
|
249
|
+
}, []);
|
|
250
|
+
|
|
251
|
+
if (!status) return <>{children}</>;
|
|
252
|
+
if (!status.connected || managing) {
|
|
253
|
+
return (
|
|
254
|
+
<div className="relative">
|
|
255
|
+
{managing && status.connected ? (
|
|
256
|
+
<Button type="button" variant="ghost" size="sm" className="absolute right-4 top-4" onClick={() => setManaging(false)}>
|
|
257
|
+
Back to the app
|
|
258
|
+
</Button>
|
|
259
|
+
) : null}
|
|
260
|
+
<ConnectGraph8Panel status={status} onChange={changed} />
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
<>
|
|
266
|
+
{children}
|
|
267
|
+
{hideManageButton ? null : (
|
|
268
|
+
<button
|
|
269
|
+
type="button"
|
|
270
|
+
onClick={() => setManaging(true)}
|
|
271
|
+
title={`Local preview, connected as ${accountLine(status, brand.linkLabel)}. Manage the ${brand.linkLabel} key.`}
|
|
272
|
+
className="fixed bottom-3 right-3 z-40 inline-flex h-7 items-center gap-1.5 rounded-md border border-border bg-background px-2 text-xs text-text-color-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
273
|
+
>
|
|
274
|
+
<KeyRound aria-hidden className="size-3.5" strokeWidth={1.5} />
|
|
275
|
+
Local preview
|
|
276
|
+
</button>
|
|
277
|
+
)}
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import { Inbox } from "lucide-react";
|
|
4
5
|
import { cn } from "@graph8/sdk/ui";
|
|
5
6
|
|
|
6
7
|
export interface EmptyStateProps {
|
|
@@ -24,6 +25,8 @@ export interface EmptyStateProps {
|
|
|
24
25
|
* these filters.", with a Clear filters action). An ink icon in a quiet well, one sentence, one action.
|
|
25
26
|
*/
|
|
26
27
|
export function EmptyState({ title, description, action, icon, className, secondaryAction, compact, bordered }: EmptyStateProps) {
|
|
28
|
+
// No icon passed: a quiet default, so an empty area never reads as a blank page. `icon={null}` hides it.
|
|
29
|
+
const mark = icon === undefined ? <Inbox strokeWidth={1.5} /> : icon;
|
|
27
30
|
return (
|
|
28
31
|
<div
|
|
29
32
|
role="status"
|
|
@@ -35,12 +38,12 @@ export function EmptyState({ title, description, action, icon, className, second
|
|
|
35
38
|
className,
|
|
36
39
|
)}
|
|
37
40
|
>
|
|
38
|
-
{
|
|
41
|
+
{mark ? (
|
|
39
42
|
<div
|
|
40
43
|
aria-hidden
|
|
41
44
|
className="mb-1 flex size-10 items-center justify-center rounded-lg border border-border bg-muted text-foreground [&_svg]:size-5 [&_svg]:stroke-[1.5]"
|
|
42
45
|
>
|
|
43
|
-
{
|
|
46
|
+
{mark}
|
|
44
47
|
</div>
|
|
45
48
|
) : null}
|
|
46
49
|
<h3 className="text-sm font-medium text-foreground">{title}</h3>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ListSelect: the audience-list dropdown, searchable and paged. It replaces a `<select>` fed by one capped
|
|
5
|
+
* `GET /lists?limit=200` read, which could not reach the 201st list. It is still a native `<select>` (keyboard,
|
|
6
|
+
* mobile and form behaviour unchanged, and the same accessible name the old select had), now with:
|
|
7
|
+
*
|
|
8
|
+
* - paging: `pageSize` lists per read (default 100); "Load more lists" appends the next page, nothing is dropped;
|
|
9
|
+
* - search: once there is more than one page, a "Search lists" box looks lists up on the server (`search=`);
|
|
10
|
+
* - the chosen list always stays an option, even when it is not on a loaded page (`valueLabel` names it).
|
|
11
|
+
*
|
|
12
|
+
* The paging engine is useLoadMoreList (the same byPage / hasNextOf contract ContactPicker's usePickerPages follows).
|
|
13
|
+
* It lives in foundation, not in the opt-in pickers block, so crm / forms / newsletter / nurture / sequencer can use
|
|
14
|
+
* it without a new block dependency.
|
|
15
|
+
*
|
|
16
|
+
* SDK operations: none here. The foundation holds no API scope of its own, so the list read comes in as `read`; the crm block
|
|
17
|
+
* ships a ready-made `ListSelect` (@/g8/crm/components/list-select) that passes GET /lists?search&page&limit (listLists, read).
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useRef, useState } from "react";
|
|
20
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
21
|
+
import { byPage } from "../lib/paging";
|
|
22
|
+
import type { RowsPage } from "../lib/paging";
|
|
23
|
+
import { useLoadMoreList } from "../hooks/use-load-more-list";
|
|
24
|
+
import { ApiError } from "./api-error";
|
|
25
|
+
|
|
26
|
+
/** A list as a select option. */
|
|
27
|
+
export interface ListSelectOption {
|
|
28
|
+
id: number;
|
|
29
|
+
title: string;
|
|
30
|
+
type?: string | null;
|
|
31
|
+
contact_count?: number | null;
|
|
32
|
+
company_count?: number | null;
|
|
33
|
+
total?: number | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reads one page of lists: `q` is the server search, `page` starts at 1. */
|
|
37
|
+
export type ListsReader = (q: string, page: number, pageSize: number, signal: AbortSignal) => Promise<RowsPage<ListSelectOption>>;
|
|
38
|
+
|
|
39
|
+
export interface ListSelectProps {
|
|
40
|
+
/** Reads a page of lists (see crm's ListSelect, which wires GET /lists). */
|
|
41
|
+
read: ListsReader;
|
|
42
|
+
/** The chosen list id (controlled); "" / null / undefined = none. */
|
|
43
|
+
value?: number | string | null;
|
|
44
|
+
/** Called with the chosen list, or null when the blank option is chosen. */
|
|
45
|
+
onChange: (list: ListSelectOption | null) => void;
|
|
46
|
+
/** Accessible name of the select (what the old select was called). */
|
|
47
|
+
label: string;
|
|
48
|
+
/** Text of the blank option ("Audience list (required)"). */
|
|
49
|
+
emptyOption?: string;
|
|
50
|
+
/** Option text for a list; default the title. */
|
|
51
|
+
optionLabel?: (list: ListSelectOption) => string;
|
|
52
|
+
/** Hide lists of the other type. Default: every list. */
|
|
53
|
+
type?: "contacts" | "companies";
|
|
54
|
+
/** Lists per read. Default 100. */
|
|
55
|
+
pageSize?: number;
|
|
56
|
+
/** Name for `value` before its list has been loaded. */
|
|
57
|
+
valueLabel?: string;
|
|
58
|
+
/** Hold the read (for example until a wizard step is reached). Default true. */
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/** Id of the select, so a <label htmlFor> can name it. */
|
|
62
|
+
id?: string;
|
|
63
|
+
className?: string;
|
|
64
|
+
/** Class of the select element itself. */
|
|
65
|
+
selectClassName?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
69
|
+
|
|
70
|
+
export function ListSelect(props: ListSelectProps) {
|
|
71
|
+
const { read, value, onChange, label, emptyOption = "Choose a list", optionLabel, type, pageSize = 100, valueLabel, enabled = true, disabled, id, className, selectClassName } = props;
|
|
72
|
+
const [search, setSearch] = useState("");
|
|
73
|
+
const [q, setQ] = useState("");
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const t = setTimeout(() => setQ(search.trim()), 250);
|
|
76
|
+
return () => clearTimeout(t);
|
|
77
|
+
}, [search]);
|
|
78
|
+
const list = useLoadMoreList(byPage<ListSelectOption>(pageSize, async (page, signal) => {
|
|
79
|
+
const got = await read(q, page, pageSize, signal);
|
|
80
|
+
return { rows: got.rows.filter((l) => !type || !l.type || l.type === type), hasNext: got.hasNext };
|
|
81
|
+
}), [q, pageSize, type], { enabled });
|
|
82
|
+
// A page that held only lists of the other type is empty after the filter but the server may have more: keep going.
|
|
83
|
+
useEffect(() => { if (list.data && list.data.length === 0 && list.hasMore && !list.loadingMore && !list.loading) list.loadMore(); }, [list.data, list.hasMore, list.loadingMore, list.loading]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
84
|
+
const pages = { rows: list.data ?? [], hasMore: list.hasMore, loadingMore: list.loadingMore, loadMore: list.loadMore, error: list.error, refetch: () => { void list.refetch(); }, q, search, setSearch };
|
|
85
|
+
const key = value == null ? "" : String(value);
|
|
86
|
+
const rows = pages.rows;
|
|
87
|
+
const known = rows.some((l) => String(l.id) === key);
|
|
88
|
+
const text = optionLabel ?? ((l: ListSelectOption) => l.title);
|
|
89
|
+
// The search box appears once the lists run past one page and then stays, so it does not vanish under the cursor.
|
|
90
|
+
const seenMore = useRef(false);
|
|
91
|
+
if (pages.hasMore) seenMore.current = true;
|
|
92
|
+
const searchable = seenMore.current || pages.q !== "" || pages.search !== "";
|
|
93
|
+
return (
|
|
94
|
+
<div className={`flex flex-col gap-1 ${className ?? ""}`}>
|
|
95
|
+
{searchable ? <Input type="text" aria-label="Search lists" placeholder="Search lists" value={pages.search} onChange={(e) => pages.setSearch(e.target.value)} className="h-8 text-sm" /> : null}
|
|
96
|
+
<select
|
|
97
|
+
id={id}
|
|
98
|
+
aria-label={label}
|
|
99
|
+
className={selectClassName ?? SELECT}
|
|
100
|
+
value={key}
|
|
101
|
+
disabled={disabled}
|
|
102
|
+
onChange={(e) => onChange(rows.find((l) => String(l.id) === e.target.value) ?? null)}
|
|
103
|
+
>
|
|
104
|
+
<option value="">{emptyOption}</option>
|
|
105
|
+
{key && !known ? <option value={key}>{valueLabel ?? `List ${key}`}</option> : null}
|
|
106
|
+
{rows.map((l) => <option key={l.id} value={l.id}>{text(l)}</option>)}
|
|
107
|
+
</select>
|
|
108
|
+
{pages.error ? <ApiError error={pages.error} title="Could not load lists" onRetry={pages.refetch} /> : null}
|
|
109
|
+
{pages.hasMore ? <Button type="button" variant="ghost" size="sm" loading={pages.loadingMore} onClick={pages.loadMore}>Load more lists</Button> : null}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FirstNotice and LoadMore: the two honest answers to a list that is longer than one read.
|
|
5
|
+
*
|
|
6
|
+
* - LoadMore: the API pages (page / offset / cursor): show how many rows are on screen and a "Load more" button.
|
|
7
|
+
* - FirstNotice: the API has no paging for this list: when the read came back full, say "Showing the first N".
|
|
8
|
+
*
|
|
9
|
+
* A capped list must never end silently; one of these is on every list read that sends a limit.
|
|
10
|
+
* SDK operations: none.
|
|
11
|
+
*/
|
|
12
|
+
import { Button, cn } from "@graph8/sdk/ui";
|
|
13
|
+
|
|
14
|
+
export interface FirstNoticeProps {
|
|
15
|
+
/** Rows on screen. */
|
|
16
|
+
shown: number;
|
|
17
|
+
/** The limit the read was sent with. */
|
|
18
|
+
limit: number;
|
|
19
|
+
/** What the rows are, plural: "mailboxes". */
|
|
20
|
+
noun: string;
|
|
21
|
+
/** Decide it yourself when the loader knows rows were cut off (useAllPages `truncated`): true shows, false hides, whatever the counts. */
|
|
22
|
+
force?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Renders only when `shown` reached `limit` (a full read: there may be more), or when `force` says so. */
|
|
27
|
+
export function FirstNotice({ shown, limit, noun, force, className }: FirstNoticeProps) {
|
|
28
|
+
if (!(force ?? shown >= limit)) return null;
|
|
29
|
+
return (
|
|
30
|
+
<p role="note" className={cn("text-xs text-muted-foreground", className)}>
|
|
31
|
+
Showing the first {(force ? shown : limit).toLocaleString()} {noun}. There may be more: narrow the list with a search or a filter to find the rest.
|
|
32
|
+
</p>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LoadMoreProps {
|
|
37
|
+
/** Rows on screen. */
|
|
38
|
+
shown: number;
|
|
39
|
+
noun: string;
|
|
40
|
+
hasMore: boolean;
|
|
41
|
+
loading?: boolean;
|
|
42
|
+
onLoadMore: () => void;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "Showing 100 landing pages" with a "Load more" button while the API has another page. */
|
|
47
|
+
export function LoadMore({ shown, noun, hasMore, loading, onLoadMore, className }: LoadMoreProps) {
|
|
48
|
+
if (!hasMore) return null;
|
|
49
|
+
return (
|
|
50
|
+
<div className={cn("flex items-center justify-between gap-3 py-2 text-sm text-muted-foreground", className)}>
|
|
51
|
+
<span role="status">Showing {shown.toLocaleString()} {noun}</span>
|
|
52
|
+
<Button type="button" variant="outline" size="sm" loading={loading} onClick={onLoadMore}>Load more</Button>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -71,7 +71,6 @@ export function DetailPanel({ title, subtitle, avatar, badges, actions, tabs, ac
|
|
|
71
71
|
{badges ? <div className="mt-1.5 flex flex-wrap items-center gap-1.5">{badges}</div> : null}
|
|
72
72
|
</div>
|
|
73
73
|
<div className="flex shrink-0 items-center gap-1">
|
|
74
|
-
{actions}
|
|
75
74
|
{onCollapse ? (
|
|
76
75
|
<Button type="button" variant="ghost" size="icon" className="size-8 text-text-color-muted hover:text-foreground" aria-label="Back to split view" onClick={onCollapse}>
|
|
77
76
|
<Minimize2 className="size-4" strokeWidth={1.5} aria-hidden />
|
|
@@ -88,6 +87,8 @@ export function DetailPanel({ title, subtitle, avatar, badges, actions, tabs, ac
|
|
|
88
87
|
) : null}
|
|
89
88
|
</div>
|
|
90
89
|
</div>
|
|
90
|
+
{/* Record actions get their own row, so the record's name is never squeezed out by them. */}
|
|
91
|
+
{actions ? <div className="mt-3 flex flex-wrap items-center gap-1.5" data-testid="detail-panel-actions">{actions}</div> : null}
|
|
91
92
|
{summary ? <div className="mt-3">{summary}</div> : null}
|
|
92
93
|
</header>
|
|
93
94
|
{tabs && tabs.length > 0 ? (
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filter controls for a Toolbar: a text box, a select, a from/to range, and a "More filters" disclosure that keeps the
|
|
5
|
+
* toolbar to one row until a person asks for the rest (it opens by itself when a hidden filter is already set).
|
|
6
|
+
* They are plain controlled inputs; useUrlFilters keeps their values in the URL.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations: none.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
11
|
+
import { Button, cn } from "@graph8/sdk/ui";
|
|
12
|
+
|
|
13
|
+
const BOX = "h-9 rounded-md border bg-background px-3 text-sm";
|
|
14
|
+
|
|
15
|
+
export interface TextFilterProps {
|
|
16
|
+
label: string;
|
|
17
|
+
value: string;
|
|
18
|
+
onChange: (value: string) => void;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
type?: "text" | "number" | "date" | "email";
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One text box; the accessible name is "Filter by <label>". */
|
|
25
|
+
export function TextFilter({ label, value, onChange, placeholder, type = "text", className }: TextFilterProps) {
|
|
26
|
+
return <input type={type} aria-label={`Filter by ${label}`} placeholder={placeholder ?? label} className={cn(BOX, "w-36", className)} value={value} onChange={(e) => onChange(e.target.value)} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SelectFilterProps {
|
|
30
|
+
label: string;
|
|
31
|
+
value: string;
|
|
32
|
+
onChange: (value: string) => void;
|
|
33
|
+
/** Text of the "no filter" option, for example "Any seniority". */
|
|
34
|
+
any: string;
|
|
35
|
+
options: ReadonlyArray<string | { value: string; label: string }>;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One select; the accessible name is "Filter by <label>". */
|
|
40
|
+
export function SelectFilter({ label, value, onChange, any, options, className }: SelectFilterProps) {
|
|
41
|
+
return (
|
|
42
|
+
<select aria-label={`Filter by ${label}`} className={cn(BOX, "px-2", className)} value={value} onChange={(e) => onChange(e.target.value)}>
|
|
43
|
+
<option value="">{any}</option>
|
|
44
|
+
{options.map((o) => { const v = typeof o === "string" ? o : o.value; return <option key={v} value={v}>{typeof o === "string" ? o : o.label}</option>; })}
|
|
45
|
+
</select>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RangeFilterProps {
|
|
50
|
+
label: string;
|
|
51
|
+
min: string;
|
|
52
|
+
max: string;
|
|
53
|
+
onMinChange: (value: string) => void;
|
|
54
|
+
onMaxChange: (value: string) => void;
|
|
55
|
+
type?: "number" | "date";
|
|
56
|
+
className?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A from / to pair; the accessible names are "<label> from" and "<label> to". */
|
|
60
|
+
export function RangeFilter({ label, min, max, onMinChange, onMaxChange, type = "number", className }: RangeFilterProps) {
|
|
61
|
+
return (
|
|
62
|
+
<span role="group" aria-label={label} className={cn("flex items-center gap-1", className)}>
|
|
63
|
+
<input type={type} aria-label={`${label} from`} placeholder={`${label} min`} className={cn(BOX, "w-28")} value={min} onChange={(e) => onMinChange(e.target.value)} />
|
|
64
|
+
<span className="text-muted-foreground text-xs" aria-hidden>to</span>
|
|
65
|
+
<input type={type} aria-label={`${label} to`} placeholder={`${label} max`} className={cn(BOX, "w-28")} value={max} onChange={(e) => onMaxChange(e.target.value)} />
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface MoreFiltersProps {
|
|
71
|
+
/** How many of the hidden filters are set (shown on the button, and opens the panel when it is above zero at mount). */
|
|
72
|
+
activeCount: number;
|
|
73
|
+
children: ReactNode;
|
|
74
|
+
label?: string;
|
|
75
|
+
className?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A "More filters" button and the second row of controls it reveals. Put it in Toolbar `filters`. */
|
|
79
|
+
export function MoreFilters({ activeCount, children, label = "More filters", className }: MoreFiltersProps) {
|
|
80
|
+
const [open, setOpen] = useState(activeCount > 0);
|
|
81
|
+
// Filters adopted from the URL right after mount (a reload, a shared link) open the panel too.
|
|
82
|
+
const hasActive = activeCount > 0;
|
|
83
|
+
useEffect(() => { if (hasActive) setOpen(true); }, [hasActive]);
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<Button type="button" variant="outline" size="sm" aria-expanded={open} onClick={() => setOpen((o) => !o)}>
|
|
87
|
+
{label}{activeCount > 0 ? ` (${activeCount})` : ""}
|
|
88
|
+
</Button>
|
|
89
|
+
{open ? <div role="group" aria-label={label} className={cn("flex w-full flex-wrap items-center gap-2", className)}>{children}</div> : null}
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
export * from "./bar-list";
|
|
10
10
|
export * from "./detail-panel";
|
|
11
11
|
export * from "./entity-avatar";
|
|
12
|
+
export * from "./filter-controls";
|
|
12
13
|
export * from "./form-sheet";
|
|
13
14
|
export * from "./key-value-list";
|
|
14
15
|
export * from "./screen-layout";
|
|
16
|
+
export * from "./record-link";
|
|
15
17
|
export * from "./section-card";
|
|
16
18
|
export * from "./sparkline";
|
|
17
19
|
export * from "./split-view";
|