@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormRouteHistory: undo a routing change. Restore the route the form had before its latest save, or stop routing the
|
|
5
|
+
* form altogether. The form keeps CAPTURING either way; only where leads are delivered changes.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. getFormOverview GET /forms/{form_key}/overview?source_id (read; the current route version and destination)
|
|
9
|
+
* 2. rollbackFormRoute DELETE /forms/{form_key}/route { source_id, expected_current_version } (DESTRUCTIVE, confirmed: restores the previous route version)
|
|
10
|
+
* 3. rollbackFormRoute DELETE /forms/{form_key}/route { source_id, cancel_current: true, expected_current_version } (DESTRUCTIVE, confirmed: removes the route)
|
|
11
|
+
*
|
|
12
|
+
* Stopping routing sends the version the screen last read, so a change made in the meantime is refused (409). Restoring the
|
|
13
|
+
* previous route is NOT version-checked by the API: it always restores the version before the current one, so look at the
|
|
14
|
+
* route version shown here before you confirm. A refused request reloads the route and says what happened.
|
|
15
|
+
*/
|
|
16
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
20
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
21
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
22
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
24
|
+
import type { FormOverview, FormRef } from "../lib/form-types";
|
|
25
|
+
|
|
26
|
+
export interface FormRouteHistoryProps {
|
|
27
|
+
form: FormRef;
|
|
28
|
+
/** Called after the route changed, so the list and overview refresh. */
|
|
29
|
+
onChanged?: () => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FormRouteHistory({ form, onChanged, className }: FormRouteHistoryProps) {
|
|
34
|
+
const base = `/forms/${encodeURIComponent(form.form_key)}`;
|
|
35
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
36
|
+
const overview = useG8Query((signal) => g8Get<FormOverview>(`${base}/overview`, { source_id: form.source_id }, { signal }).then((r) => r.data), [form.form_key, form.source_id]);
|
|
37
|
+
const version = overview.data?.route?.version ?? undefined;
|
|
38
|
+
const list = overview.data?.destination?.list_name;
|
|
39
|
+
const done = (): void => { void overview.refetch(); onChanged?.(); };
|
|
40
|
+
const conflict = (err: unknown): unknown => {
|
|
41
|
+
if ((err as { status?: number })?.status !== 409) return err;
|
|
42
|
+
void overview.refetch();
|
|
43
|
+
return new Error("The API refused this: the route changed since it was loaded, or there is no earlier version to restore. The route shown here was reloaded: check it and try again.");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<SectionCard className={className} title="Undo a routing change" description="Where this form's leads are delivered.">
|
|
48
|
+
{overview.error ? <ApiError error={overview.error} title="Could not load the route" onRetry={() => void overview.refetch()} /> : !overview.data ? <Skeleton className="h-16 w-full" aria-busy="true" /> : version === undefined ? (
|
|
49
|
+
<p className="text-sm">This form has no destination route to undo.</p>
|
|
50
|
+
) : (
|
|
51
|
+
<div className="grid gap-3">
|
|
52
|
+
<KeyValueList columns={2} items={[{ label: "Route version", value: String(version) }, { label: "Destination list", value: list ?? "" }]} />
|
|
53
|
+
<div className="flex flex-wrap gap-2">
|
|
54
|
+
<Button variant="outline" disabled={version < 2} onClick={() => ask({
|
|
55
|
+
tier: "destructive", title: "Restore the previous route", confirmLabel: "Restore previous route",
|
|
56
|
+
description: `Replaces the current destination and field mapping with the ones this form had before its latest save. New leads follow the restored route; leads already delivered are not moved.`,
|
|
57
|
+
mapError: conflict, run: () => g8Delete(`${base}/route`, { body: { source_id: form.source_id } }), onDone: done,
|
|
58
|
+
})}>Restore the previous route</Button>
|
|
59
|
+
<Button variant="ghost" onClick={() => ask({
|
|
60
|
+
tier: "destructive", title: "Stop routing this form", confirmLabel: "Stop routing",
|
|
61
|
+
description: "Removes the destination and mapping. The form keeps capturing submissions, but they are not delivered to any list until you set a destination again.",
|
|
62
|
+
mapError: conflict, run: () => g8Delete(`${base}/route`, { body: { source_id: form.source_id, cancel_current: true, expected_current_version: version } }), onDone: done,
|
|
63
|
+
})}>Stop routing this form</Button>
|
|
64
|
+
</div>
|
|
65
|
+
{version < 2 ? <p className="text-muted-foreground text-xs">There is no earlier route to restore.</p> : null}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
{failure ? <ApiError error={failure} title="That did not work" onRetry={() => void overview.refetch()} /> : null}
|
|
69
|
+
<ConfirmSpendDialog {...dialog} />
|
|
70
|
+
</SectionCard>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FormSavedViews: save the current search and health filter of the forms list under a name, and bring it back with one
|
|
5
|
+
* click.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* 1. listSavedFormViews GET /forms/views/saved (read)
|
|
9
|
+
* 2. saveFormView PUT /forms/views/saved/{view_id} { name, filters } (write; a new view gets a fresh id, saving over a view keeps its id)
|
|
10
|
+
*
|
|
11
|
+
* The filters saved are the ones this list has: `search` and, for failing / needs setup / live, `saved_view`. Applying a
|
|
12
|
+
* view hands those back to the screen; nothing is written when a view is applied.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { g8Get, g8Put } from "@/g8/lib/g8-client";
|
|
20
|
+
import { asRec, rowsOf, str } from "../lib/forms-extra";
|
|
21
|
+
import type { Rec } from "../lib/forms-extra";
|
|
22
|
+
|
|
23
|
+
export interface FormSavedViewsProps {
|
|
24
|
+
/** The list's current search text. */
|
|
25
|
+
search: string;
|
|
26
|
+
/** The list's current health filter ("" for all). */
|
|
27
|
+
health: string;
|
|
28
|
+
/** Apply a saved view back to the list. */
|
|
29
|
+
onApply: (view: { search: string; health: string }) => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Health values the API's `saved_view` filter understands. */
|
|
34
|
+
const HEALTH_VIEWS = new Set(["failing", "needs_setup", "live"]);
|
|
35
|
+
|
|
36
|
+
/** What a saved `saved_view` becomes in this list's health filter; values the list has no filter for map to "all". */
|
|
37
|
+
export function healthOfView(view: string): string {
|
|
38
|
+
if (view === "failing" || view === "failures") return "failing";
|
|
39
|
+
if (view === "needs_setup") return "needs_setup";
|
|
40
|
+
if (view === "live" || view === "healthy") return "live";
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The list filters a view cannot hold (paused, no data): saving would silently drop them. */
|
|
45
|
+
export const UNSAVABLE_HEALTH = new Set(["paused", "no_data"]);
|
|
46
|
+
|
|
47
|
+
export function savedFilters(search: string, health: string): Rec {
|
|
48
|
+
return { ...(search.trim() ? { search: search.trim() } : {}), ...(HEALTH_VIEWS.has(health) ? { saved_view: health } : {}) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function newViewId(): string {
|
|
52
|
+
return globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : `view-${Date.now().toString(36)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function FormSavedViews({ search, health, onApply, className }: FormSavedViewsProps) {
|
|
56
|
+
const views = useG8Query((signal) => g8Get("/forms/views/saved", undefined, { signal }), []);
|
|
57
|
+
const rows = rowsOf<Rec>(views.data?.data, "views");
|
|
58
|
+
const [name, setName] = useState("");
|
|
59
|
+
const filters = savedFilters(search, health);
|
|
60
|
+
const existing = rows.find((v) => str(v.name) === name.trim());
|
|
61
|
+
// Saving under a name that already exists keeps that view's id (an update); a new name gets a fresh id.
|
|
62
|
+
const save = useG8Mutation(() => g8Put(`/forms/views/saved/${encodeURIComponent(existing ? str(existing.id) : newViewId())}`, { name: name.trim(), filters }), {
|
|
63
|
+
onSuccess: () => { setName(""); void views.refetch(); },
|
|
64
|
+
});
|
|
65
|
+
return (
|
|
66
|
+
<div className={className}>
|
|
67
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
68
|
+
{rows.map((v) => {
|
|
69
|
+
const f = asRec(v.filters);
|
|
70
|
+
return <Button key={str(v.id)} size="sm" variant="outline" onClick={() => onApply({ search: str(f.search), health: healthOfView(str(f.saved_view)) })}>{str(v.name)}</Button>;
|
|
71
|
+
})}
|
|
72
|
+
<Input aria-label="Name for this view" className="h-8 w-40" placeholder="Save this view as" value={name} onChange={(e) => setName(e.target.value)} />
|
|
73
|
+
<Button size="sm" variant="outline" loading={save.loading} disabled={name.trim() === "" || Object.keys(filters).length === 0 || UNSAVABLE_HEALTH.has(health)} onClick={() => void save.mutate(undefined)}>Save view</Button>
|
|
74
|
+
</div>
|
|
75
|
+
{UNSAVABLE_HEALTH.has(health) ? <p role="note" className="text-muted-foreground mt-1 text-xs">A view cannot hold the Paused or No data filter. Pick another health, or clear it, to save this view.</p> : null}
|
|
76
|
+
{Object.keys(filters).length === 0 && name.trim() !== "" ? <p className="text-muted-foreground mt-1 text-xs">Set a search or a health filter first: a view with no filters would show everything.</p> : null}
|
|
77
|
+
{views.error ? <p role="note" className="text-muted-foreground mt-1 text-xs">Saved views could not be loaded. <button type="button" className="underline" onClick={() => void views.refetch()}>Reload views</button></p> : null}
|
|
78
|
+
{save.error ? <ApiError error={save.error} title="Could not save the view" /> : null}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
* Composes: ScreenLayout (title, tabs, stats), Toolbar (debounced search, health filter), SplitView +
|
|
10
10
|
* DetailPanel (selected form, deep link `?id=<source_id>/<form_key>`, one submission `&submission=<id>`), FormEmbed
|
|
11
11
|
* (Install tab, `?tab=install`), SiteAuditPanel (Site audit tab, `?tab=audit`), FormMappingEditor (Setup tab),
|
|
12
|
-
* SubmissionsTable (Submissions tab)
|
|
12
|
+
* SubmissionsTable + SubmissionsExport (Submissions tab), FormBackfill + FormRouteHistory (Routing tab), FormActivity
|
|
13
|
+
* (Activity tab), FormGroups (Groups tab, `?tab=groups`), FormSavedViews (saved search and health filters).
|
|
13
14
|
*
|
|
14
15
|
* User flows: install the snippet and copy the ready-made lead form, see every captured form with its health and
|
|
15
16
|
* counts, search and filter, open one to read capture and delivery numbers, rename it, pause or resume delivery
|
|
16
17
|
* (confirmed), pick the destination list and map fields (suggest, preview, save, test), read submissions, trace
|
|
17
|
-
* where a lead went and retry a failed delivery (confirmed),
|
|
18
|
+
* where a lead went and retry a failed delivery (confirmed), export submissions to CSV, deliver earlier leads through
|
|
19
|
+
* the route (preview, then a confirmed delivery), undo a routing change (confirmed), read a form's activity and the pages
|
|
20
|
+
* it appears on, group related forms, save a filtered view, load every form past the first page, and find forms graph8
|
|
21
|
+
* is not capturing. Creating a form is not possible: forms are captured from your site (an API limit, not a missing screen).
|
|
18
22
|
*
|
|
19
23
|
* SDK operations: listForms, getFormOverview, updateFormRegistry, pauseFormDelivery / resumeFormDelivery
|
|
20
24
|
* (external, confirmed), searchFormSubmissions (read, to resolve a `&submission=` deep link that was not clicked
|
|
@@ -41,11 +45,20 @@ import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
|
41
45
|
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
42
46
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
43
47
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
48
|
+
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
49
|
+
import { pageOf } from "../lib/forms-extra";
|
|
44
50
|
import { STATUS_LABEL, cellText, formLabel, formStatus, pageUrlText, pickItems, submissionFailed, submissionState } from "../lib/form-types";
|
|
45
51
|
import type { FormListItem, FormOverview, FormRef, FormSubmission } from "../lib/form-types";
|
|
46
52
|
import { FormEmbed } from "./form-embed";
|
|
53
|
+
import { FormActivity } from "./form-activity";
|
|
54
|
+
import { FormBackfill } from "./form-backfill";
|
|
55
|
+
import { FormGroups } from "./form-groups";
|
|
47
56
|
import { FormMappingEditor } from "./form-mapping-editor";
|
|
57
|
+
import { FormRouteHistory } from "./form-route-history";
|
|
58
|
+
import { FormSavedViews } from "./form-saved-views";
|
|
59
|
+
import { MoreRows } from "./more-rows";
|
|
48
60
|
import { SiteAuditPanel } from "./site-audit-panel";
|
|
61
|
+
import { SubmissionsExport } from "./submissions-export";
|
|
49
62
|
import { SubmissionsTable } from "./submissions-table";
|
|
50
63
|
import { pluralWord } from "@/g8/lib/plural";
|
|
51
64
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
@@ -53,12 +66,14 @@ import { useBrand } from "@/g8/hooks/use-brand";
|
|
|
53
66
|
export interface FormsScreenProps {
|
|
54
67
|
renderRowActions?: (form: FormListItem) => ReactNode;
|
|
55
68
|
renderDetailActions?: (form: FormListItem) => ReactNode;
|
|
69
|
+
/** Extra buttons on an opened submission, next to its delivery state (open or add the person who submitted it). */
|
|
70
|
+
renderSubmissionActions?: (submission: FormSubmission, form: FormListItem) => ReactNode;
|
|
56
71
|
extraDetailTabs?: { id: string; label: string; render: (form: FormListItem) => ReactNode }[];
|
|
57
72
|
toolbarActions?: ReactNode;
|
|
58
73
|
className?: string;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
type Tab = "forms" | "install" | "audit";
|
|
76
|
+
type Tab = "forms" | "install" | "audit" | "groups";
|
|
62
77
|
const LIMIT = 100;
|
|
63
78
|
/** How many of a form's latest submissions a deep link is looked up in. */
|
|
64
79
|
const SUBMISSION_LOOKUP = 100;
|
|
@@ -151,7 +166,7 @@ function SubmissionNotFound({ form, submissionId }: { form: FormRef; submissionI
|
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
/** One submission in full: every submitted field, where it came from, its delivery, and the trace. */
|
|
154
|
-
function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; submission: FormSubmission; onChanged: () => void }) {
|
|
169
|
+
function SubmissionDetail({ form, submission, onChanged, actions }: { form: FormRef; submission: FormSubmission; onChanged: () => void; actions?: ReactNode }) {
|
|
155
170
|
const [retrying, setRetrying] = useState(false);
|
|
156
171
|
const trace = useG8Query(
|
|
157
172
|
(signal) => g8Get<{ capture_state?: string; receipts?: unknown[] }>(`/forms/${encodeURIComponent(form.form_key)}/submissions/${encodeURIComponent(submission.id)}/trace`, { source_id: form.source_id }, { signal }).then((r) => r.data),
|
|
@@ -167,6 +182,7 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
|
|
|
167
182
|
<StatusPill tone={failed ? "negative" : submissionState(submission) === "delivered" ? "positive" : "neutral"}>{submissionState(submission)}</StatusPill>
|
|
168
183
|
{submission.is_test ? <StatusPill tone="info">Test</StatusPill> : null}
|
|
169
184
|
{failed && submission.delivery?.receipt_id ? <Button size="sm" variant="outline" onClick={() => { retry.reset(); setRetrying(true); }}>Retry delivery</Button> : null}
|
|
185
|
+
{actions}
|
|
170
186
|
</div>
|
|
171
187
|
<SectionCard title="Submitted fields">
|
|
172
188
|
{fields.length === 0 ? <p className="text-muted-foreground text-sm">No fields were captured. Identifying fields may be redacted for your key.</p> : (
|
|
@@ -205,10 +221,10 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
|
|
|
205
221
|
);
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
export function FormsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
|
|
224
|
+
export function FormsScreen({ renderRowActions, renderDetailActions, renderSubmissionActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
|
|
209
225
|
const brand = useBrand();
|
|
210
226
|
const [tabParam, setTabParam] = useUrlState("tab", "forms");
|
|
211
|
-
const tab: Tab = tabParam === "install" || tabParam === "audit" ? tabParam : "forms";
|
|
227
|
+
const tab: Tab = tabParam === "install" || tabParam === "audit" || tabParam === "groups" ? tabParam : "forms";
|
|
212
228
|
const setTab = (next: Tab): void => setTabParam(next === "forms" ? null : next);
|
|
213
229
|
const [selectedKey, setSelectedKey] = useUrlState("id", "");
|
|
214
230
|
const [view, setView] = useUrlState("view", "");
|
|
@@ -227,12 +243,18 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
227
243
|
}, [text]);
|
|
228
244
|
|
|
229
245
|
const closeForm = (): void => { setView(null); setSelectedKey(null); setSubmissionId(null); setOpenedSubmission(null); };
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
246
|
+
// Every page of forms is reachable: the list loads LIMIT at a time and "Load more" appends the next page.
|
|
247
|
+
const [apiTotal, setTotal] = useState(0);
|
|
248
|
+
const forms = usePaginated<FormListItem>(async (cursor, signal) => {
|
|
249
|
+
const page = cursor ? Number(cursor) : 1;
|
|
250
|
+
const r = await g8Get<{ items?: FormListItem[]; total?: number }>("/forms", { page, per_page: LIMIT, search: search || undefined }, { signal });
|
|
251
|
+
const got = pickItems<FormListItem>(r.data, "items");
|
|
252
|
+
const all = (r.data as { total?: number } | undefined)?.total ?? 0;
|
|
253
|
+
setTotal(all);
|
|
254
|
+
return pageOf(got, got.length > 0 && page * LIMIT < all ? page + 1 : null);
|
|
255
|
+
}, [search]);
|
|
256
|
+
const items = forms.items;
|
|
257
|
+
const total = apiTotal || items.length;
|
|
236
258
|
const shown = items.filter((f) => !health || formStatus(f) === health);
|
|
237
259
|
const ref = parseRef(selectedKey);
|
|
238
260
|
const selected: FormListItem | undefined = ref ? (items.find((f) => f.form_key === ref.form_key && f.source_id === ref.source_id) ?? { id: `${ref.source_id}:${ref.form_key}`, ...ref }) : undefined;
|
|
@@ -250,7 +272,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
250
272
|
);
|
|
251
273
|
const linkedSubmission: FormSubmission | null = !submissionId ? null : openedSubmission?.id === submissionId ? openedSubmission : ((lookup.data ?? []).find((x) => x.id === submissionId) ?? null);
|
|
252
274
|
|
|
253
|
-
const rows = forms.error ? <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.
|
|
275
|
+
const rows = forms.error ? <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.reload()} /> : forms.loading && forms.items.length === 0 ? (
|
|
254
276
|
<div aria-busy="true" className="grid gap-2"><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /><Skeleton className="h-16 w-full" /></div>
|
|
255
277
|
) : shown.length === 0 ? (
|
|
256
278
|
<EmptyState
|
|
@@ -278,7 +300,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
278
300
|
</li>
|
|
279
301
|
);
|
|
280
302
|
})}
|
|
281
|
-
{
|
|
303
|
+
{forms.hasMore ? <li><MoreRows shown={items.length} noun={`of ${total.toLocaleString()} forms`} hasMore loading={forms.loadingMore} onMore={() => void forms.loadMore()} /></li> : null}
|
|
282
304
|
</ul>
|
|
283
305
|
);
|
|
284
306
|
|
|
@@ -291,9 +313,21 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
291
313
|
onClose={() => closeForm()}
|
|
292
314
|
{...expandProps}
|
|
293
315
|
tabs={[
|
|
294
|
-
{ id: "overview", label: "Overview", content: <FormOverviewTab key={refKey(selected)} form={selected} onChanged={() => void forms.
|
|
295
|
-
{ id: "setup", label: "Destination and fields", content: <FormMappingEditor key={refKey(selected)} form={selected} onSaved={() => void forms.
|
|
296
|
-
{ id: "submissions", label: "Submissions", content:
|
|
316
|
+
{ id: "overview", label: "Overview", content: <FormOverviewTab key={refKey(selected)} form={selected} onChanged={() => void forms.reload()} /> },
|
|
317
|
+
{ id: "setup", label: "Destination and fields", content: <FormMappingEditor key={refKey(selected)} form={selected} onSaved={() => void forms.reload()} /> },
|
|
318
|
+
{ id: "submissions", label: "Submissions", content: (
|
|
319
|
+
<div className="grid gap-3">
|
|
320
|
+
<SubmissionsExport form={selected} />
|
|
321
|
+
<SubmissionsTable key={`${refKey(selected)}:${subsRefresh}`} form={selected} selectedId={submissionId || undefined} onOpen={(sub) => { setOpenedSubmission(sub); setSubmissionId(sub.id); }} />
|
|
322
|
+
</div>
|
|
323
|
+
) },
|
|
324
|
+
{ id: "routing", label: "Routing", content: (
|
|
325
|
+
<div className="grid gap-4">
|
|
326
|
+
<FormBackfill key={`b${refKey(selected)}`} form={selected} />
|
|
327
|
+
<FormRouteHistory key={`r${refKey(selected)}`} form={selected} onChanged={() => void forms.reload()} />
|
|
328
|
+
</div>
|
|
329
|
+
) },
|
|
330
|
+
{ id: "activity", label: "Activity", content: <FormActivity key={refKey(selected)} form={selected} /> },
|
|
297
331
|
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
|
|
298
332
|
]}
|
|
299
333
|
/>
|
|
@@ -305,7 +339,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
305
339
|
title="Forms"
|
|
306
340
|
description="Capture the forms already on your site, route every lead to a list, and watch what arrives."
|
|
307
341
|
icon={<ClipboardList className="size-5" />}
|
|
308
|
-
tabs={[{ id: "forms", label: "Forms", count: total }, { id: "install", label: "Install" }, { id: "audit", label: "Site audit" }]}
|
|
342
|
+
tabs={[{ id: "forms", label: "Forms", count: total }, { id: "groups", label: "Groups" }, { id: "install", label: "Install" }, { id: "audit", label: "Site audit" }]}
|
|
309
343
|
activeTab={tab}
|
|
310
344
|
onTabChange={(id) => { setTab(id as Tab); closeForm(); }}
|
|
311
345
|
stats={tab === "forms" ? [
|
|
@@ -315,6 +349,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
315
349
|
{ label: "Need setup", value: items.filter((f) => formStatus(f) === "needs_setup").length.toLocaleString(), tone: "info" },
|
|
316
350
|
] : undefined}
|
|
317
351
|
toolbar={tab === "forms" ? (
|
|
352
|
+
<>
|
|
318
353
|
<Toolbar
|
|
319
354
|
search={{ value: text, onChange: setText, placeholder: "Search forms" }}
|
|
320
355
|
filters={(
|
|
@@ -325,6 +360,8 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
325
360
|
)}
|
|
326
361
|
actions={toolbarActions}
|
|
327
362
|
/>
|
|
363
|
+
<FormSavedViews className="mt-2" search={text} health={health} onApply={(v) => { setText(v.search); setSearch(v.search.trim()); setHealth(v.health); }} />
|
|
364
|
+
</>
|
|
328
365
|
) : undefined}
|
|
329
366
|
>
|
|
330
367
|
<FormSheet
|
|
@@ -335,7 +372,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
335
372
|
>
|
|
336
373
|
{selected && submissionId ? (
|
|
337
374
|
linkedSubmission ? (
|
|
338
|
-
<SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} />
|
|
375
|
+
<SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} actions={renderSubmissionActions?.(linkedSubmission, selected)} />
|
|
339
376
|
) : lookup.error ? (
|
|
340
377
|
<ApiError error={lookup.error} title="Could not load this submission" onRetry={() => void lookup.refetch()} />
|
|
341
378
|
) : lookup.loading || !lookup.data ? (
|
|
@@ -345,7 +382,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
345
382
|
)
|
|
346
383
|
) : null}
|
|
347
384
|
</FormSheet>
|
|
348
|
-
{tab === "install" ? <FormEmbed /> : tab === "audit" ? <SiteAuditPanel /> : (
|
|
385
|
+
{tab === "install" ? <FormEmbed /> : tab === "audit" ? <SiteAuditPanel /> : tab === "groups" ? <FormGroups /> : (
|
|
349
386
|
<SplitView list={rows} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={() => closeForm()} expanded={view === "full"} onExpandedChange={(e) => setView(e ? "full" : null)} detailWidth="xl" />
|
|
350
387
|
)}
|
|
351
388
|
</ScreenLayout>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MoreRows: the honest end of a list that the API pages. Shows how many rows are on screen and, while the API has
|
|
5
|
+
* another page, a "Load more" button. A list never ends silently: either every row is reachable or this says there are more.
|
|
6
|
+
* (Same contract as the foundation's LoadMore; kept local so the forms block does not wait on it.)
|
|
7
|
+
*
|
|
8
|
+
* SDK operations: none.
|
|
9
|
+
*/
|
|
10
|
+
import { Button } from "@graph8/sdk/ui";
|
|
11
|
+
|
|
12
|
+
export interface MoreRowsProps {
|
|
13
|
+
/** Rows on screen. */
|
|
14
|
+
shown: number;
|
|
15
|
+
/** What the rows are, plural: "people". */
|
|
16
|
+
noun: string;
|
|
17
|
+
hasMore: boolean;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
onMore: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function MoreRows({ shown, noun, hasMore, loading, onMore }: MoreRowsProps) {
|
|
23
|
+
if (!hasMore) return null;
|
|
24
|
+
return (
|
|
25
|
+
<div className="text-muted-foreground flex items-center justify-between gap-3 py-2 text-sm">
|
|
26
|
+
<span role="status">Showing {shown.toLocaleString()} {noun}</span>
|
|
27
|
+
<Button type="button" variant="outline" size="sm" loading={loading} onClick={onMore}>Load more</Button>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SubmissionsExport: download a form's submissions as a CSV file (one column per submitted field).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* 1. searchFormSubmissions POST /forms/{form_key}/submissions { page, per_page: 100, source_id } (read; every page is fetched in turn)
|
|
8
|
+
*
|
|
9
|
+
* The export fetches up to `maxRows` submissions (default 5,000) and says so when the form has more: a truncated file
|
|
10
|
+
* is never presented as the whole thing. Identifying fields are redacted for keys without the Signals write capability,
|
|
11
|
+
* and the file carries the redacted values. Cells that start with = + - or @ are prefixed with ' so a spreadsheet does
|
|
12
|
+
* not run them as formulas.
|
|
13
|
+
*/
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Download } from "lucide-react";
|
|
16
|
+
import { Button } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
20
|
+
import { submissionState } from "../lib/form-types";
|
|
21
|
+
import type { FormRef, FormSubmission } from "../lib/form-types";
|
|
22
|
+
import { submissionsCsv } from "../lib/forms-extra";
|
|
23
|
+
|
|
24
|
+
export interface SubmissionsExportProps {
|
|
25
|
+
form: FormRef;
|
|
26
|
+
/** The most submissions one file holds. Default 5000. */
|
|
27
|
+
maxRows?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PER_PAGE = 100;
|
|
32
|
+
|
|
33
|
+
export function SubmissionsExport({ form, maxRows = 5000, className }: SubmissionsExportProps) {
|
|
34
|
+
const [note, setNote] = useState("");
|
|
35
|
+
const run = useG8Mutation(async () => {
|
|
36
|
+
const rows: FormSubmission[] = [];
|
|
37
|
+
let total = 0;
|
|
38
|
+
for (let page = 1; rows.length < maxRows; page += 1) {
|
|
39
|
+
setNote(`Collecting submissions: ${rows.length.toLocaleString()}${total ? ` of ${total.toLocaleString()}` : ""}`);
|
|
40
|
+
const res = await g8Post<unknown>(`/forms/${encodeURIComponent(form.form_key)}/submissions`, { page, per_page: PER_PAGE, source_id: form.source_id });
|
|
41
|
+
const d = res.data as { submissions?: { items?: FormSubmission[]; total?: number }; items?: FormSubmission[]; total?: number } | undefined;
|
|
42
|
+
const block = d?.submissions ?? d;
|
|
43
|
+
const items = block?.items ?? [];
|
|
44
|
+
total = block?.total ?? total;
|
|
45
|
+
rows.push(...items);
|
|
46
|
+
if (items.length < PER_PAGE || (total && rows.length >= total)) break;
|
|
47
|
+
}
|
|
48
|
+
const file = rows.slice(0, maxRows);
|
|
49
|
+
const csv = submissionsCsv(file.map((s) => ({ id: s.id, received_at: s.received_at, is_test: s.is_test, delivery_state: submissionState(s), form_data: s.form_data })));
|
|
50
|
+
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
|
|
51
|
+
const a = document.createElement("a");
|
|
52
|
+
a.href = url;
|
|
53
|
+
a.download = `${form.form_key.replace(/[^a-z0-9-_]+/gi, "-").toLowerCase() || "form"}-submissions.csv`;
|
|
54
|
+
a.click();
|
|
55
|
+
URL.revokeObjectURL(url);
|
|
56
|
+
setNote(total > file.length ? `Downloaded the first ${file.length.toLocaleString()} of ${total.toLocaleString()} submissions. The file does not hold the rest.` : `Downloaded ${file.length.toLocaleString()} submissions.`);
|
|
57
|
+
});
|
|
58
|
+
return (
|
|
59
|
+
<div className={className}>
|
|
60
|
+
<div className="flex items-center gap-3">
|
|
61
|
+
<Button size="sm" variant="outline" loading={run.loading} onClick={() => void run.mutate(undefined)}><Download className="size-4" />Export CSV</Button>
|
|
62
|
+
{note ? <span role="status" className="text-muted-foreground text-xs">{note}</span> : null}
|
|
63
|
+
</div>
|
|
64
|
+
{run.error ? <ApiError error={run.error} title="Could not export the submissions" /> : null}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -18,6 +18,7 @@ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
|
18
18
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
19
|
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
20
20
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
21
22
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
23
|
import { cellText, pickItems, submissionFailed, submissionState } from "../lib/form-types";
|
|
23
24
|
import type { FormRef, FormSubmission } from "../lib/form-types";
|
|
@@ -64,19 +65,27 @@ function SubmissionTrace({ form, submission, onClose }: { form: FormRef; submiss
|
|
|
64
65
|
);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/** Forms per read: every page is read, so the form picker offers all of them. */
|
|
69
|
+
const FORM_PAGE = 100;
|
|
70
|
+
|
|
67
71
|
export function SubmissionsTable({ form: given, pageSize = 20, maxFieldColumns = 4, onOpen, selectedId }: SubmissionsTableProps) {
|
|
68
72
|
const brand = useBrand();
|
|
69
73
|
const [picked, setPicked] = useState<FormRef | undefined>(undefined);
|
|
70
|
-
const forms =
|
|
74
|
+
const forms = useAllPages<FormRef & { display_name?: string | null }>(async (page, signal) => {
|
|
75
|
+
const r = await g8Get<unknown>("/forms", { page, per_page: FORM_PAGE }, { signal });
|
|
76
|
+
const rows = pickItems<FormRef & { display_name?: string | null }>(r.data, "items");
|
|
77
|
+
const total = (r.data as { total?: number } | undefined)?.total;
|
|
78
|
+
return { rows, hasNext: typeof total === "number" ? page * FORM_PAGE < total : rows.length >= FORM_PAGE };
|
|
79
|
+
}, [], { enabled: !given });
|
|
71
80
|
const form = given ?? picked;
|
|
72
81
|
if (!form) {
|
|
73
82
|
if (forms.error) return <ApiError error={forms.error} title="Could not load forms" onRetry={() => void forms.refetch()} />;
|
|
74
|
-
if (forms.loading && !forms.
|
|
75
|
-
if (
|
|
83
|
+
if (forms.loading && !forms.loaded) return <Skeleton className="h-9 w-64" aria-busy="true" />;
|
|
84
|
+
if (forms.items.length === 0) return <EmptyState title="No forms captured yet" description={`Install the ${brand.linkLabel} snippet on your site; forms appear after their first submission.`} />;
|
|
76
85
|
return (
|
|
77
|
-
<select aria-label="Choose a form" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" defaultValue="" onChange={(e) => { const f = forms.
|
|
86
|
+
<select aria-label="Choose a form" className="border-input bg-background h-9 max-w-sm rounded-md border px-2 text-sm" defaultValue="" onChange={(e) => { const f = forms.items[Number(e.target.value)]; if (f) setPicked({ form_key: f.form_key, source_id: f.source_id }); }}>
|
|
78
87
|
<option value="" disabled>Choose a form</option>
|
|
79
|
-
{
|
|
88
|
+
{forms.items.map((f, i) => <option key={`${f.source_id}:${f.form_key}`} value={i}>{f.display_name || f.form_key}</option>)}
|
|
80
89
|
</select>
|
|
81
90
|
);
|
|
82
91
|
}
|
|
@@ -136,7 +145,7 @@ function SubmissionsFor({ form, pageSize, maxFieldColumns, onOpen, selectedId }:
|
|
|
136
145
|
) : <>{s.received_at ? new Date(s.received_at).toLocaleString() : ""}{s.is_test ? " (test)" : ""}</>}
|
|
137
146
|
</TableCell>
|
|
138
147
|
{columns.map((c) => <TableCell key={c}>{cellText(s.form_data?.[c])}</TableCell>)}
|
|
139
|
-
<TableCell className={submissionFailed(s) ? "text-
|
|
148
|
+
<TableCell className={submissionFailed(s) ? "text-error-foreground" : undefined}>{submissionState(s)}</TableCell>
|
|
140
149
|
<TableCell className="text-right">
|
|
141
150
|
<Button size="sm" variant="ghost" onClick={() => setTraceOf(s)}>Trace</Button>
|
|
142
151
|
{submissionFailed(s) && s.delivery?.receipt_id ? <Button size="sm" variant="ghost" onClick={() => { retry.reset(); setRetrying(s); }}>Retry</Button> : null}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helpers for the forms management components (groups, activity, backfill, route history, saved views, export).
|
|
5
|
+
* These endpoints return open dicts, so every reader accepts what is present and ignores the rest.
|
|
6
|
+
*/
|
|
7
|
+
import type { PaginatedResponse } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
export type Rec = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export function asRec(value: unknown): Rec {
|
|
12
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Rec) : {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function str(value: unknown, fallback = ""): string {
|
|
16
|
+
return typeof value === "string" ? value : value == null ? fallback : String(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function num(value: unknown): number | undefined {
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function rowsOf<T = Rec>(data: unknown, ...keys: string[]): T[] {
|
|
24
|
+
if (Array.isArray(data)) return data as T[];
|
|
25
|
+
const rec = asRec(data);
|
|
26
|
+
for (const key of [...keys, "items"]) if (Array.isArray(rec[key])) return rec[key] as T[];
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Wrap rows and the API's "next" marker (a page number, cursor or offset; null when there is no more) for `usePaginated`. */
|
|
31
|
+
export function pageOf<T>(rows: T[], next: unknown): PaginatedResponse<T> {
|
|
32
|
+
return { data: rows, pagination: { next_cursor: next == null || next === "" ? null : String(next) } } as unknown as PaginatedResponse<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const when = (v: unknown): string => {
|
|
36
|
+
const d = new Date(str(v));
|
|
37
|
+
return Number.isNaN(d.getTime()) ? "" : d.toLocaleString();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `YYYY-MM-DD` from a date input to the UTC instant the API takes: start of that day, or the start of the NEXT day for an inclusive end. */
|
|
41
|
+
export function dayInstant(day: string, endOfDay = false): string | undefined {
|
|
42
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return undefined;
|
|
43
|
+
const d = new Date(`${day}T00:00:00Z`);
|
|
44
|
+
if (Number.isNaN(d.getTime())) return undefined;
|
|
45
|
+
if (endOfDay) d.setUTCDate(d.getUTCDate() + 1);
|
|
46
|
+
return d.toISOString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A member of a form group, under whichever key names it used. */
|
|
50
|
+
export function memberRef(m: Rec): { form_key: string; source_id: string } {
|
|
51
|
+
return { form_key: str(m.form_key), source_id: str(m.source_id) || str(m.source_key) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One CSV cell: quoted when it holds a comma, quote or newline, and guarded against spreadsheet formulas. */
|
|
55
|
+
export function csvCell(value: unknown): string {
|
|
56
|
+
let s = value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
57
|
+
if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
|
58
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The CSV text for submissions: a fixed leading set of columns, then every submitted field seen, in first-seen order. */
|
|
62
|
+
export function submissionsCsv(rows: Array<{ id: string; received_at?: string; is_test?: boolean; delivery_state: string; form_data?: Record<string, unknown> }>): string {
|
|
63
|
+
const fields = [...new Set(rows.flatMap((r) => Object.keys(r.form_data ?? {})))];
|
|
64
|
+
const head = ["submission_id", "received_at", "test", "delivery", ...fields];
|
|
65
|
+
const lines = rows.map((r) => [r.id, r.received_at ?? "", r.is_test ? "yes" : "no", r.delivery_state, ...fields.map((f) => r.form_data?.[f])].map(csvCell).join(","));
|
|
66
|
+
return [head.map(csvCell).join(","), ...lines].join("\r\n");
|
|
67
|
+
}
|