@graph8/sdk 0.370.0 → 0.374.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 +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -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 +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- 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 +53 -11
- 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/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -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 +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- 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 +165 -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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- 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 +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- 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 +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- 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 +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- 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 +57 -20
- 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 +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -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 +20 -1
- 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 +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- 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 +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- 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 +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- 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/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- 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 +38 -13
- 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/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- 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-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/hooks/use-landing-page-paths.ts +60 -0
- 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/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- 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 +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- 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 +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -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-screen.tsx +33 -6
- 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/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- 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 +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- 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 +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- 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 +34 -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 +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- 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/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- 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 +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- 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 +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -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
|
+
}
|
|
@@ -95,7 +95,7 @@ export function SplitView({ list, detail, detailOpen, onCloseDetail, detailWidth
|
|
|
95
95
|
|
|
96
96
|
return (
|
|
97
97
|
<div className={cn("flex h-full min-h-0 w-full min-w-0", className)} data-testid="split-view" data-detail-open={open ? "true" : "false"}>
|
|
98
|
-
<div className={cn("min-h-0 min-w-0 flex-1", listClassName)}>{list}</div>
|
|
98
|
+
<div className={cn("min-h-0 min-w-0 flex-1", listClassName)} data-g8-list="">{list}</div>
|
|
99
99
|
{open && !narrow ? (
|
|
100
100
|
<aside
|
|
101
101
|
aria-label={detailTitle}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useAllPages: read every page of a list that a screen needs IN FULL (a lookup by id, a dropdown that must offer every
|
|
5
|
+
* row), instead of one call with a hopeful `limit`. Pages are fetched one after the other until the API says there is no
|
|
6
|
+
* next page, or `maxPages` is reached; then `truncated` is true and the screen must say so (see FirstNotice).
|
|
7
|
+
*
|
|
8
|
+
* It calls no operation itself: `fetchPage` does, so the caller's component declares the operation.
|
|
9
|
+
*/
|
|
10
|
+
import { useG8Query } from "./use-g8-query";
|
|
11
|
+
import type { RowsPage } from "../lib/paging";
|
|
12
|
+
|
|
13
|
+
export interface UseAllPagesOptions {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
/** Stop after this many pages (default 20) so a runaway list cannot loop forever. */
|
|
16
|
+
maxPages?: number;
|
|
17
|
+
/** The page size you read with: when a page does not say `hasNext`, a full page of this size means "maybe more". */
|
|
18
|
+
pageSize?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseAllPages<T> {
|
|
22
|
+
items: T[];
|
|
23
|
+
loading: boolean;
|
|
24
|
+
/** The first read has finished (a row list may still be empty). */
|
|
25
|
+
loaded: boolean;
|
|
26
|
+
error: unknown;
|
|
27
|
+
/** The page limit was reached with more rows still on the server: some rows are NOT in `items`. */
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
refetch: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface All<T> { items: T[]; truncated: boolean }
|
|
33
|
+
|
|
34
|
+
export function useAllPages<T>(
|
|
35
|
+
fetchPage: (page: number, signal: AbortSignal) => Promise<RowsPage<T>>,
|
|
36
|
+
deps: readonly unknown[],
|
|
37
|
+
options: UseAllPagesOptions = {},
|
|
38
|
+
): UseAllPages<T> {
|
|
39
|
+
const max = options.maxPages ?? 20;
|
|
40
|
+
const q = useG8Query<All<T>>(async (signal) => {
|
|
41
|
+
const items: T[] = [];
|
|
42
|
+
for (let page = 1; page <= max; page++) {
|
|
43
|
+
const got = await fetchPage(page, signal);
|
|
44
|
+
items.push(...got.rows);
|
|
45
|
+
if (got.hasNext === false || got.rows.length === 0) return { items, truncated: false };
|
|
46
|
+
if (got.hasNext === undefined && (options.pageSize === undefined || got.rows.length < options.pageSize)) return { items, truncated: false }; // could not tell, and not a full page
|
|
47
|
+
}
|
|
48
|
+
return { items, truncated: true };
|
|
49
|
+
}, deps, { enabled: options.enabled });
|
|
50
|
+
return { items: q.data?.items ?? EMPTY, loading: q.loading, loaded: q.data !== undefined, error: q.error, truncated: q.data?.truncated ?? false, refetch: () => { void q.refetch(); } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
const EMPTY: any[] = [];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useLoadMoreList: a list read that never stops at its first page. It has the shape of a plain `useG8Query` list
|
|
5
|
+
* (`data`, `loading`, `error`, `refetch`) plus `hasMore` / `loadMore` / `loadingMore`, so a screen that read one capped
|
|
6
|
+
* page can adopt it by swapping the hook and rendering <LoadMore> under the rows.
|
|
7
|
+
*
|
|
8
|
+
* const list = useLoadMoreList(byOffset(100, async (offset, signal) => {
|
|
9
|
+
* const env = await g8Get("/landing-pages", { limit: 100, offset }, { signal });
|
|
10
|
+
* const rows = listOf(env.data);
|
|
11
|
+
* return { rows, hasNext: hasNextOf(env, 100, rows.length) };
|
|
12
|
+
* }), [status]);
|
|
13
|
+
* ...
|
|
14
|
+
* <LoadMore shown={rows.length} noun="landing pages" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
15
|
+
*
|
|
16
|
+
* Built on usePaginated (changing `deps` starts over, stale responses are dropped). It calls no operation itself.
|
|
17
|
+
*/
|
|
18
|
+
import { useRef } from "react";
|
|
19
|
+
import { usePaginated } from "./use-paginated";
|
|
20
|
+
import type { UsePaginatedOptions } from "./use-paginated";
|
|
21
|
+
import type { PaginatedResponse } from "@graph8/sdk";
|
|
22
|
+
|
|
23
|
+
export interface UseLoadMoreList<T> {
|
|
24
|
+
/** The rows loaded so far; undefined until the first page arrives. */
|
|
25
|
+
data: T[] | undefined;
|
|
26
|
+
loading: boolean;
|
|
27
|
+
loadingMore: boolean;
|
|
28
|
+
error: unknown;
|
|
29
|
+
hasMore: boolean;
|
|
30
|
+
loadMore: () => void;
|
|
31
|
+
/** Start over from the first page; resolves with that first page of rows (what a poll needs to decide), undefined if it failed. */
|
|
32
|
+
refetch: () => Promise<T[] | undefined>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useLoadMoreList<T>(
|
|
36
|
+
fetchPage: (cursor: string | undefined, signal: AbortSignal) => Promise<PaginatedResponse<T>>,
|
|
37
|
+
deps: readonly unknown[],
|
|
38
|
+
options: UsePaginatedOptions = {},
|
|
39
|
+
): UseLoadMoreList<T> {
|
|
40
|
+
const first = useRef<T[] | undefined>(undefined);
|
|
41
|
+
const p = usePaginated<T>(async (cursor, signal) => {
|
|
42
|
+
const page = await fetchPage(cursor, signal);
|
|
43
|
+
if (cursor === undefined) first.current = [...(page.data ?? [])];
|
|
44
|
+
return page;
|
|
45
|
+
}, deps, options);
|
|
46
|
+
return {
|
|
47
|
+
data: p.loading && p.items.length === 0 ? undefined : p.items,
|
|
48
|
+
loading: p.loading,
|
|
49
|
+
loadingMore: p.loadingMore,
|
|
50
|
+
error: p.error,
|
|
51
|
+
hasMore: p.hasMore,
|
|
52
|
+
loadMore: () => { void p.loadMore(); },
|
|
53
|
+
// Resolves with the FIRST page of the new load (first.current is set by the wrapped fetcher above), not the whole list:
|
|
54
|
+
// enough for a poll that only asks "is anything still generating?".
|
|
55
|
+
refetch: async () => {
|
|
56
|
+
first.current = undefined;
|
|
57
|
+
await p.reload();
|
|
58
|
+
return first.current;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useUrlFilters: the filter state of a list screen, kept in the URL so a filtered view survives reload, Back and sharing.
|
|
5
|
+
*
|
|
6
|
+
* const f = useUrlFilters(["email", "city"] as const, { prefix: "c_" });
|
|
7
|
+
* <input value={f.draft.email} onChange={(e) => f.set("email", e.target.value)} />
|
|
8
|
+
* g8Get("/contacts", { page, ...f.query }); // f.query holds only the filters that are set, debounced
|
|
9
|
+
*
|
|
10
|
+
* Typing is local (`draft`, instant); the debounced `filters` are what the list reads, and they are written to the URL as
|
|
11
|
+
* `<prefix><key>=...`. A URL that already carries filters is the starting value, with no flash of an unfiltered list and
|
|
12
|
+
* no wipe at mount. It is read through `useSyncExternalStore` (like useUrlState), so it is hydration-safe: the server and
|
|
13
|
+
* the first hydration render both see empty filters, React then re-renders with the URL's. In that first hydration render
|
|
14
|
+
* `ready` is false: hold the list read until it is true (`enabled: filters.ready`), so no unfiltered read goes out. On a
|
|
15
|
+
* client-only render `ready` is true from the start. `clear()` empties everything. Params of other hooks (id, view, tab)
|
|
16
|
+
* are never touched.
|
|
17
|
+
*
|
|
18
|
+
* SDK operations: none.
|
|
19
|
+
*/
|
|
20
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
21
|
+
import { readUrlParam, writeUrlParam } from "./use-url-state";
|
|
22
|
+
|
|
23
|
+
export interface UseUrlFiltersOptions {
|
|
24
|
+
/** URL param prefix, so two screens on one page do not share names. Default "". */
|
|
25
|
+
prefix?: string;
|
|
26
|
+
/** Milliseconds of quiet typing before the list reads the new filters. Default 300. */
|
|
27
|
+
debounceMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UseUrlFilters<K extends string> {
|
|
31
|
+
/** What the inputs show (instant). */
|
|
32
|
+
draft: Record<K, string>;
|
|
33
|
+
/** What the list reads (debounced after an edit). */
|
|
34
|
+
filters: Record<K, string>;
|
|
35
|
+
/** Only the filters that are set, ready to spread into a query. */
|
|
36
|
+
query: Partial<Record<K, string>>;
|
|
37
|
+
set: (key: K, value: string) => void;
|
|
38
|
+
clear: () => void;
|
|
39
|
+
/** Some filter is set (draft). */
|
|
40
|
+
active: boolean;
|
|
41
|
+
/** How many of `keys` are set (draft). */
|
|
42
|
+
count: number;
|
|
43
|
+
/** The URL's filters have been read (false only in the first hydration render): hold the list read until true. */
|
|
44
|
+
ready: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const noSubscribe = (): (() => void) => () => {};
|
|
48
|
+
|
|
49
|
+
export function useUrlFilters<K extends string>(keys: readonly K[], options: UseUrlFiltersOptions = {}): UseUrlFilters<K> {
|
|
50
|
+
const prefix = options.prefix ?? "";
|
|
51
|
+
const ms = options.debounceMs ?? 300;
|
|
52
|
+
const sig = keys.join(",");
|
|
53
|
+
const empty = useCallback((): Record<K, string> => Object.fromEntries(keys.map((k) => [k, ""])) as Record<K, string>, [sig]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
54
|
+
|
|
55
|
+
// The URL's filters as one string, frozen at the first client read. The server (and the first hydration render) sees "".
|
|
56
|
+
const frozen = useRef<string | null>(null);
|
|
57
|
+
const urlSnap = useSyncExternalStore(
|
|
58
|
+
noSubscribe,
|
|
59
|
+
() => (frozen.current ??= JSON.stringify(keys.map((k) => readUrlParam(prefix + k) ?? ""))),
|
|
60
|
+
() => JSON.stringify(keys.map(() => "")),
|
|
61
|
+
);
|
|
62
|
+
const ready = useSyncExternalStore(noSubscribe, () => true, () => false);
|
|
63
|
+
const fromUrl = useMemo(() => {
|
|
64
|
+
const values = JSON.parse(urlSnap) as string[];
|
|
65
|
+
return Object.fromEntries(keys.map((k, i) => [k, values[i] ?? ""])) as Record<K, string>;
|
|
66
|
+
}, [urlSnap, sig]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
|
+
|
|
68
|
+
// `edited` is null until the person types: until then the filters ARE the URL's (no debounce lag, nothing to write back).
|
|
69
|
+
const [edited, setEdited] = useState<Record<K, string> | null>(null);
|
|
70
|
+
const [settled, setSettled] = useState<Record<K, string> | null>(null);
|
|
71
|
+
const draft = edited ?? fromUrl;
|
|
72
|
+
const filters = settled ?? fromUrl;
|
|
73
|
+
const latest = useRef(draft);
|
|
74
|
+
latest.current = draft;
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (edited === null) return;
|
|
78
|
+
const t = setTimeout(() => setSettled(edited), ms);
|
|
79
|
+
return () => clearTimeout(t);
|
|
80
|
+
}, [edited, ms]);
|
|
81
|
+
|
|
82
|
+
// The settled filters are the URL's value; writeUrlParam skips an unchanged value, so mounting writes nothing.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!ready) return;
|
|
85
|
+
for (const k of keys) writeUrlParam(prefix + k, filters[k] || null);
|
|
86
|
+
}, [ready, filters]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
87
|
+
|
|
88
|
+
const set = useCallback((key: K, value: string) => setEdited({ ...latest.current, [key]: value }), []);
|
|
89
|
+
const clear = useCallback(() => setEdited(empty()), [empty]);
|
|
90
|
+
const query = Object.fromEntries(keys.filter((k) => filters[k]).map((k) => [k, filters[k]])) as Partial<Record<K, string>>;
|
|
91
|
+
const count = keys.filter((k) => draft[k]).length;
|
|
92
|
+
return { draft, filters, query, set, clear, active: count > 0, count, ready };
|
|
93
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Paging helpers: adapt the API's several paging dialects (`page`, `offset`, `cursor`) to the one shape
|
|
5
|
+
* `usePaginated` loads ("load more" appends the next page). No operation is called here; the caller's component
|
|
6
|
+
* performs and declares the read.
|
|
7
|
+
*
|
|
8
|
+
* const list = usePaginated(byOffset(PAGE, async (offset, signal) => {
|
|
9
|
+
* const env = await g8Get("/landing-pages", { limit: PAGE, offset }, { signal });
|
|
10
|
+
* return { rows: listOf(env.data), hasNext: hasNextOf(env, PAGE) };
|
|
11
|
+
* }), [status]);
|
|
12
|
+
*/
|
|
13
|
+
import type { PaginatedResponse } from "@graph8/sdk";
|
|
14
|
+
|
|
15
|
+
/** One page of rows and whether another follows (omit `hasNext` and a full page is taken to mean "maybe more"). */
|
|
16
|
+
export interface RowsPage<T> {
|
|
17
|
+
rows: T[];
|
|
18
|
+
hasNext?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Fetcher<T> = (cursor: string | undefined, signal: AbortSignal) => Promise<PaginatedResponse<T>>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `trustHasNext`: when the page says `hasNext: true` believe it even if it carries no rows (a page-number list whose rows
|
|
25
|
+
* were filtered client-side can be empty while the server still has more). Offset lists cannot: an empty page does not
|
|
26
|
+
* advance the offset, so they stop on an empty page.
|
|
27
|
+
*/
|
|
28
|
+
function toResponse<T>(page: RowsPage<T>, size: number, next: number, trustHasNext = false): PaginatedResponse<T> {
|
|
29
|
+
const more = page.hasNext ?? page.rows.length >= size;
|
|
30
|
+
const go = more && (page.rows.length > 0 || (trustHasNext && page.hasNext === true));
|
|
31
|
+
return { data: page.rows, pagination: { next_cursor: go ? String(next) : null } } as unknown as PaginatedResponse<T>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `?page=N` lists: the cursor carries the next page number. */
|
|
35
|
+
export function byPage<T>(size: number, read: (page: number, signal: AbortSignal) => Promise<RowsPage<T>>): Fetcher<T> {
|
|
36
|
+
return async (cursor, signal) => {
|
|
37
|
+
const page = cursor ? Number(cursor) || 1 : 1;
|
|
38
|
+
return toResponse(await read(page, signal), size, page + 1, true);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `?offset=N` (or skip) lists: the cursor carries the next offset. */
|
|
43
|
+
export function byOffset<T>(size: number, read: (offset: number, signal: AbortSignal) => Promise<RowsPage<T>>): Fetcher<T> {
|
|
44
|
+
return async (cursor, signal) => {
|
|
45
|
+
const offset = cursor ? Number(cursor) || 0 : 0;
|
|
46
|
+
const page = await read(offset, signal);
|
|
47
|
+
return toResponse(page, size, offset + page.rows.length);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Whether an API envelope says another page follows: `pagination.has_next`, then `next_cursor`, then a full page. */
|
|
52
|
+
export function hasNextOf(env: unknown, size: number, rows?: number): boolean | undefined {
|
|
53
|
+
const p = (env as { pagination?: { has_next?: boolean; next_cursor?: string | null; total?: number; page?: number } } | null)?.pagination;
|
|
54
|
+
if (typeof p?.has_next === "boolean") return p.has_next;
|
|
55
|
+
if (p && "next_cursor" in p && p.next_cursor !== undefined) return Boolean(p.next_cursor);
|
|
56
|
+
return rows === undefined ? undefined : rows >= size;
|
|
57
|
+
}
|
|
@@ -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` (foundation-local: the pickers block has its own copy). */
|
|
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
|
+
}
|