@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,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticGenerate: render every page of a campaign (one per source row) and follow the run.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getProgrammaticWorkspace (GET /programmatic-campaigns/{id}/workspace) read how many source rows there are, and the approval mode
|
|
8
|
+
* 2. startProgrammaticGeneration (POST /programmatic-campaigns/{id}/generate) write background run; FREE, no model runs
|
|
9
|
+
* 3. getProgrammaticGenerationStatus (GET /programmatic-campaigns/{id}/status) read polled until the run settles
|
|
10
|
+
* Generating is free. It is also the one write that can reach the live site: when the campaign's approval mode is "Publish
|
|
11
|
+
* automatically" the API marks every page live as it renders, so that case is confirmed (tier external) with the number of
|
|
12
|
+
* source rows. The API does not clear earlier pages first, so running again on a campaign that already has pages may add a
|
|
13
|
+
* second set; the panel says so before the button is pressed. A run already in progress, or an empty source, is refused by
|
|
14
|
+
* the API and the button is disabled with the reason.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState } from "react";
|
|
18
|
+
import { Play } from "lucide-react";
|
|
19
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { EmptyValue } from "@/g8/components/screen/key-value-list";
|
|
23
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
26
|
+
import { invalidate } from "@/g8/lib/g8-cache";
|
|
27
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import { plural } from "@/g8/lib/plural";
|
|
29
|
+
import { useProgrammaticWorkspace } from "../hooks/use-programmatic-workspace";
|
|
30
|
+
import { isGenerating } from "../lib/programmatic-types";
|
|
31
|
+
import type { Campaign, GenerationStatus } from "../lib/programmatic-types";
|
|
32
|
+
|
|
33
|
+
export interface ProgrammaticGenerateProps {
|
|
34
|
+
campaign: Campaign;
|
|
35
|
+
/** Called when a run settles (completed, failed or paused). */
|
|
36
|
+
onSettled?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SETTLED = ["completed", "failed", "paused", "draft"];
|
|
40
|
+
|
|
41
|
+
export function ProgrammaticGenerate({ campaign, onSettled }: ProgrammaticGenerateProps) {
|
|
42
|
+
const workspace = useProgrammaticWorkspace(campaign.id);
|
|
43
|
+
const [asking, setAsking] = useState(false);
|
|
44
|
+
const base = `/programmatic-campaigns/${encodeURIComponent(campaign.id)}`;
|
|
45
|
+
const poll = useJobPoll<GenerationStatus>(
|
|
46
|
+
(signal) => g8Get<GenerationStatus>(`${base}/status`, {}, { signal }).then((r) => r.data),
|
|
47
|
+
{ enabled: false, isTerminal: (s) => SETTLED.includes(String(s.status)), intervalMs: 1500, onDone: () => { void invalidate(["programmatic"]); onSettled?.(); } },
|
|
48
|
+
);
|
|
49
|
+
const start = poll.start;
|
|
50
|
+
useEffect(() => { if (isGenerating(campaign)) start(); }, [campaign.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
51
|
+
|
|
52
|
+
const run = useG8Mutation(() => g8Post(`${base}/generate`), {
|
|
53
|
+
onSuccess: () => { setAsking(false); void invalidate(["programmatic"]); start(); },
|
|
54
|
+
onError: () => setAsking(false),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const rows = workspace.data?.data?.total_rows ?? workspace.data?.overview?.total_rows ?? null;
|
|
58
|
+
const approval = (workspace.data?.campaign?.regeneration?.approval_mode ?? campaign.regeneration?.approval_mode) ?? "";
|
|
59
|
+
const publishes = approval === "auto_publish";
|
|
60
|
+
const running = poll.polling || isGenerating(campaign) && !poll.done;
|
|
61
|
+
const hasPages = (campaign.pages_generated ?? 0) + (campaign.pages_failed ?? 0) > 0;
|
|
62
|
+
const empty = rows === 0;
|
|
63
|
+
const live = poll.status;
|
|
64
|
+
const generated = live?.pages_generated ?? campaign.pages_generated ?? null;
|
|
65
|
+
const failed = live?.pages_failed ?? campaign.pages_failed ?? null;
|
|
66
|
+
const total = live?.total_pages ?? campaign.total_pages ?? null;
|
|
67
|
+
const pct = live?.progress_pct ?? (total ? Math.round(((generated ?? 0) / total) * 100) : null);
|
|
68
|
+
|
|
69
|
+
if (workspace.loading && !workspace.data) return <Skeleton className="h-24 w-full" aria-busy="true" />;
|
|
70
|
+
const disabledReason = running ? "A run is already in progress." : empty ? "The source has no rows, so there is nothing to render." : "";
|
|
71
|
+
return (
|
|
72
|
+
<section aria-label="Generate pages" className="grid gap-3 rounded-md border p-3">
|
|
73
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
74
|
+
<div>
|
|
75
|
+
<h3 className="text-sm font-medium">Render the pages</h3>
|
|
76
|
+
<p className="text-muted-foreground text-xs">One page per source row ({rows == null ? <EmptyValue /> : plural(rows, "row")}), rendered by filling in the template. Free: no model runs.</p>
|
|
77
|
+
</div>
|
|
78
|
+
<Button size="sm" loading={run.loading} disabled={Boolean(disabledReason) || run.loading} title={disabledReason || undefined} onClick={() => (publishes ? (run.reset(), setAsking(true)) : void run.mutate(undefined))}>
|
|
79
|
+
<Play className="size-4" />{hasPages ? "Generate again" : "Generate pages"}
|
|
80
|
+
</Button>
|
|
81
|
+
</div>
|
|
82
|
+
{disabledReason ? <p role="note" className="text-muted-foreground text-xs">{disabledReason}</p> : null}
|
|
83
|
+
{publishes ? <p role="note" className="text-xs">This campaign publishes automatically: pages are marked live as they render.</p> : <p className="text-muted-foreground text-xs">Pages are created as drafts. Nothing goes live until you publish them.</p>}
|
|
84
|
+
{hasPages && !running ? <p role="note" className="text-xs">This campaign already has pages. The API does not clear them first, so running again may add a second set of the same pages.</p> : null}
|
|
85
|
+
{running || live ? (
|
|
86
|
+
<div className="grid gap-1">
|
|
87
|
+
<div role="progressbar" aria-label="Generation progress" aria-valuemin={0} aria-valuemax={100} aria-valuenow={pct ?? undefined} className="bg-muted h-2 overflow-hidden rounded-full">
|
|
88
|
+
<div className="bg-foreground h-full" style={{ width: `${Math.min(100, Math.max(0, pct ?? 0))}%` }} />
|
|
89
|
+
</div>
|
|
90
|
+
<p role="status" className="text-muted-foreground text-xs">
|
|
91
|
+
{generated == null ? <EmptyValue /> : generated.toLocaleString()} of {total == null ? <EmptyValue /> : total.toLocaleString()} rendered
|
|
92
|
+
{failed ? `, ${failed.toLocaleString()} failed` : ""} <StatusPill tone={live?.status === "failed" ? "negative" : live?.status === "completed" ? "positive" : "info"}>{live?.status ?? campaign.status ?? "generating"}</StatusPill>
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
) : null}
|
|
96
|
+
{live?.error_message ? <p role="alert" className="text-error text-xs">{live.error_message}</p> : null}
|
|
97
|
+
{poll.error ? <ApiError error={poll.error} title="Could not read the progress" onRetry={start} /> : null}
|
|
98
|
+
{run.error ? <ApiError error={run.error} title="Could not start the run" /> : null}
|
|
99
|
+
<ConfirmSpendDialog
|
|
100
|
+
open={asking}
|
|
101
|
+
tier="external"
|
|
102
|
+
title={`Render and publish ${rows == null ? "every" : plural(rows ?? 0, "page")}?`}
|
|
103
|
+
affected={rows ?? undefined}
|
|
104
|
+
description={<p>This campaign's approval mode is "Publish automatically". Each page is marked live as soon as it renders, with its public address. Switch the approval mode to "Draft only" in Settings to review first.</p>}
|
|
105
|
+
confirmLabel="Render and publish"
|
|
106
|
+
confirming={run.loading}
|
|
107
|
+
onCancel={() => setAsking(false)}
|
|
108
|
+
onConfirm={() => void run.mutate(undefined)}
|
|
109
|
+
/>
|
|
110
|
+
</section>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticGeneratedPages: the generated pages of one campaign, with row and bulk lifecycle actions.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. listProgrammaticGeneratedPages (GET /programmatic-campaigns/{id}/pages) read 50 per read, offset paged
|
|
8
|
+
* 2. runProgrammaticPageAction (POST /programmatic-campaigns/{id}/pages/{page_id}/actions) external mark ready, unpublish, mark for
|
|
9
|
+
* regeneration, replace the HTML (override), undo the replacement. Unpublish takes the page OFF the live site: confirmed.
|
|
10
|
+
* 3. runProgrammaticBulkPageAction (POST /programmatic-campaigns/{id}/pages/actions) external the same on a selection; delete
|
|
11
|
+
* removes pages (destructive, confirmed); queue_search_console submits live URLs to Google (confirmed); export_urls only reads.
|
|
12
|
+
* A confirm counts exactly the pages the request sends: the selection is filtered once to the pages the action applies to (live
|
|
13
|
+
* pages for unpublish and Search Console, rendered drafts for mark ready) and that array is both counted and sent. The list is
|
|
14
|
+
* paged by the API without a filter, so search and the status filter work on the pages loaded so far and say so.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
18
|
+
import { ExternalLink, FileDown, Trash2 } from "lucide-react";
|
|
19
|
+
import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
24
|
+
import { EmptyValue } from "@/g8/components/screen/key-value-list";
|
|
25
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
26
|
+
import { SelectFilter } from "@/g8/components/screen/filter-controls";
|
|
27
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
28
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
29
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
30
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
31
|
+
import { invalidate } from "@/g8/lib/g8-cache";
|
|
32
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
33
|
+
import { byOffset } from "@/g8/lib/paging";
|
|
34
|
+
import { plural } from "@/g8/lib/plural";
|
|
35
|
+
import { asRec, num, rowsOf, shortDate } from "@/g8/pages/lib/studio";
|
|
36
|
+
import { PAGE_SIZE, PAGE_STATUSES, bulkTargets, isLive, pageActionBlock, wording } from "../lib/programmatic-types";
|
|
37
|
+
import type { BulkActionName, Campaign, GeneratedPage, PageActionName } from "../lib/programmatic-types";
|
|
38
|
+
|
|
39
|
+
export interface ProgrammaticGeneratedPagesProps {
|
|
40
|
+
campaign: Campaign;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Pending = { kind: "page"; page: GeneratedPage; action: "unpublish" } | { kind: "bulk"; action: BulkActionName; ids: string[] };
|
|
44
|
+
const tone = (s?: string | null) => (s === "published" ? "positive" : s === "failed" ? "negative" : s === "generated" ? "info" : "neutral");
|
|
45
|
+
const LABELS: Record<string, string> = { mark_ready_to_deploy: "Mark ready", unpublish: "Unpublish", mark_for_regeneration: "Regenerate", override: "Replace HTML", reset_override: "Undo HTML edit" };
|
|
46
|
+
|
|
47
|
+
export function ProgrammaticGeneratedPages({ campaign }: ProgrammaticGeneratedPagesProps) {
|
|
48
|
+
const base = `/programmatic-campaigns/${encodeURIComponent(campaign.id)}`;
|
|
49
|
+
const total = useRef<number | undefined>(undefined);
|
|
50
|
+
const list = useLoadMoreList(byOffset<GeneratedPage>(PAGE_SIZE, async (offset, signal) => {
|
|
51
|
+
const data = asRec((await g8Get(`${base}/pages`, { limit: PAGE_SIZE, offset: offset || undefined }, { signal })).data);
|
|
52
|
+
const rows = rowsOf<GeneratedPage>(data, "pages");
|
|
53
|
+
if (offset === 0) total.current = num(data.total); // the first read of a campaign replaces the count; a later page never keeps another campaign's
|
|
54
|
+
return { rows, hasNext: total.current === undefined ? undefined : offset + rows.length < total.current };
|
|
55
|
+
}), [campaign.id]);
|
|
56
|
+
const pages = list.data ?? [];
|
|
57
|
+
const [search, setSearch] = useState("");
|
|
58
|
+
const [status, setStatus] = useState("");
|
|
59
|
+
const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());
|
|
60
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
61
|
+
const [editing, setEditing] = useState<GeneratedPage | null>(null);
|
|
62
|
+
const [html, setHtml] = useState("");
|
|
63
|
+
const [busy, setBusy] = useState<string | null>(null);
|
|
64
|
+
const [note, setNote] = useState("");
|
|
65
|
+
const [urls, setUrls] = useState<string[] | null>(null);
|
|
66
|
+
|
|
67
|
+
// Selection follows the data: ids that are no longer loaded (a refetch, a delete) leave the selection.
|
|
68
|
+
useEffect(() => { setSelected((cur) => { const alive = new Set(pages.map((p) => p.id)); const next = new Set([...cur].filter((id) => alive.has(id))); return next.size === cur.size ? cur : next; }); }, [list.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
69
|
+
useEffect(() => { setSelected(new Set()); setNote(""); }, [campaign.id]);
|
|
70
|
+
|
|
71
|
+
const shown = useMemo(() => {
|
|
72
|
+
const q = search.trim().toLowerCase();
|
|
73
|
+
return pages.filter((p) => (!status || p.status === status) && (!q || `${p.slug ?? ""} ${p.published_url ?? ""}`.toLowerCase().includes(q)));
|
|
74
|
+
}, [pages, search, status]);
|
|
75
|
+
|
|
76
|
+
const refresh = (): void => { void invalidate(["programmatic"]); void list.refetch(); };
|
|
77
|
+
const rowAction = useG8Mutation((v: { page: GeneratedPage; action: PageActionName; html?: string }) => {
|
|
78
|
+
const body: Record<string, unknown> = { action: v.action };
|
|
79
|
+
if (v.action === "override") { body.html_output = v.html; body.manual_override = { fields: ["html_output"] }; }
|
|
80
|
+
return g8Post(`${base}/pages/${encodeURIComponent(v.page.id)}/actions`, body);
|
|
81
|
+
}, { onSuccess: (_r, v) => { setBusy(null); setPending(null); setEditing(null); setNote(`${LABELS[v.action]} done for ${v.page.slug ?? "the page"}.`); refresh(); }, onError: () => setBusy(null) });
|
|
82
|
+
const bulk = useG8Mutation((v: { action: BulkActionName; ids: string[] }) => g8Post<{ urls?: string[]; count?: number }>(`${base}/pages/actions`, { action: v.action, page_ids: v.ids }), {
|
|
83
|
+
onSuccess: (res, v) => {
|
|
84
|
+
setPending(null);
|
|
85
|
+
const d = asRec(res.data);
|
|
86
|
+
if (v.action === "export_urls") setUrls(Array.isArray(d.urls) ? (d.urls as string[]) : []);
|
|
87
|
+
else { setSelected(new Set()); setNote(`${plural(num(d.count) ?? v.ids.length, "page")} updated.`); refresh(); }
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const run = (page: GeneratedPage, action: PageActionName): void => {
|
|
92
|
+
rowAction.reset();
|
|
93
|
+
if (action === "unpublish") return setPending({ kind: "page", page, action });
|
|
94
|
+
if (action === "override") { setHtml(""); return setEditing(page); }
|
|
95
|
+
setBusy(page.id);
|
|
96
|
+
void rowAction.mutate({ page, action });
|
|
97
|
+
};
|
|
98
|
+
const ask = (action: BulkActionName): void => {
|
|
99
|
+
const ids = bulkTargets(pages, selected, action);
|
|
100
|
+
if (ids.length === 0) return;
|
|
101
|
+
if (action === "unpublish_selected" || action === "delete_selected" || action === "queue_search_console") { bulk.reset(); setPending({ kind: "bulk", action, ids }); }
|
|
102
|
+
else void bulk.mutate({ action, ids });
|
|
103
|
+
};
|
|
104
|
+
const toggle = (id: string): void => setSelected((cur) => { const n = new Set(cur); if (n.has(id)) n.delete(id); else n.add(id); return n; });
|
|
105
|
+
const allShown = shown.length > 0 && shown.every((p) => selected.has(p.id));
|
|
106
|
+
const count = (a: BulkActionName): number => bulkTargets(pages, selected, a).length;
|
|
107
|
+
|
|
108
|
+
const copy = pending?.kind === "bulk" ? pending : null;
|
|
109
|
+
const liveInPick = (ids: string[]): number => bulkTargets(pages, new Set(ids), "unpublish_selected").length;
|
|
110
|
+
const confirmText = pending?.kind === "page"
|
|
111
|
+
? { tier: "external" as const, title: `Take ${pending.page.slug ?? "this page"} off the live site?`, body: "Visitors to its address will no longer see it. On a custom domain the last published copy may stay up until the next publish. You can mark it ready and publish it again.", label: "Unpublish", affected: 1 }
|
|
112
|
+
: copy?.action === "delete_selected"
|
|
113
|
+
? { tier: "destructive" as const, title: `Delete ${plural(copy.ids.length, "page")}?`, body: `The pages are removed from this campaign. ${liveInPick(copy.ids) === 0 ? "None of them is live." : `${plural(liveInPick(copy.ids), "of them is", "of them are")} live.`} On a hosted address a deleted live page stops being served right away; on a custom domain its last published copy may stay up until the next publish. Unpublish first if you only want it offline.`, label: "Delete", affected: copy.ids.length }
|
|
114
|
+
: copy?.action === "queue_search_console"
|
|
115
|
+
? { tier: "external" as const, title: `Send ${plural(copy.ids.length, "live page")} to Google Search Console?`, body: "The live addresses are queued for submission to the connected Search Console property.", label: "Send", affected: copy.ids.length }
|
|
116
|
+
: copy
|
|
117
|
+
? { tier: "external" as const, title: `Unpublish ${plural(copy.ids.length, "live page")}?`, body: "Visitors to their addresses will no longer see them (on a custom domain the last published copy may stay up until the next publish). Pages that are not live are left out.", label: "Unpublish", affected: copy.ids.length }
|
|
118
|
+
: null;
|
|
119
|
+
const confirming = rowAction.loading || bulk.loading;
|
|
120
|
+
|
|
121
|
+
if (list.error && !list.data) return <ApiError error={list.error} title="Could not load the pages" onRetry={() => void list.refetch()} />;
|
|
122
|
+
if (!list.data) return <div aria-busy="true" className="grid gap-2"><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>;
|
|
123
|
+
if (pages.length === 0) return <EmptyState title="No pages yet" description="Render the pages from the Overview tab. One page is made for every row of the source." />;
|
|
124
|
+
return (
|
|
125
|
+
<div className="grid gap-3">
|
|
126
|
+
<Toolbar
|
|
127
|
+
search={{ value: search, onChange: setSearch, placeholder: "Search loaded pages" }}
|
|
128
|
+
filters={<SelectFilter label="page status" value={status} onChange={setStatus} any="Any status" options={PAGE_STATUSES.map((s) => ({ value: s, label: wording(s) }))} />}
|
|
129
|
+
/>
|
|
130
|
+
<p role="status" className="text-muted-foreground text-xs">
|
|
131
|
+
{shown.length === pages.length ? `${pages.length.toLocaleString()} of ${(total.current ?? pages.length).toLocaleString()} pages loaded` : `${shown.length.toLocaleString()} of ${pages.length.toLocaleString()} loaded pages match`}
|
|
132
|
+
{list.hasMore ? ". Search and the filter look only at the pages loaded so far." : ""}
|
|
133
|
+
</p>
|
|
134
|
+
{selected.size > 0 ? (
|
|
135
|
+
<div role="group" aria-label="Actions for the selected pages" className="bg-muted flex flex-wrap items-center gap-2 rounded-md p-2 text-sm">
|
|
136
|
+
<span className="mr-2">{plural(selected.size, "page")} selected</span>
|
|
137
|
+
<Button size="sm" variant="outline" disabled={count("mark_ready_to_deploy_selected") === 0} loading={bulk.loading} onClick={() => ask("mark_ready_to_deploy_selected")}>Mark {count("mark_ready_to_deploy_selected")} ready</Button>
|
|
138
|
+
<Button size="sm" variant="outline" disabled={count("mark_for_regeneration_selected") === 0} onClick={() => ask("mark_for_regeneration_selected")}>Regenerate {count("mark_for_regeneration_selected")}</Button>
|
|
139
|
+
<Button size="sm" variant="outline" disabled={count("unpublish_selected") === 0} onClick={() => ask("unpublish_selected")}>Unpublish {count("unpublish_selected")}</Button>
|
|
140
|
+
<Button size="sm" variant="outline" disabled={count("queue_search_console") === 0} onClick={() => ask("queue_search_console")}>Send {count("queue_search_console")} to Search Console</Button>
|
|
141
|
+
<Button size="sm" variant="outline" disabled={count("export_urls") === 0} onClick={() => ask("export_urls")}><FileDown className="size-4" />Export {count("export_urls")} addresses</Button>
|
|
142
|
+
<Button size="sm" variant="outline" onClick={() => ask("delete_selected")}><Trash2 className="size-4" />Delete {count("delete_selected")}</Button>
|
|
143
|
+
</div>
|
|
144
|
+
) : null}
|
|
145
|
+
{note ? <p role="status" className="text-xs">{note}</p> : null}
|
|
146
|
+
{!pending && bulk.error ? <ApiError error={bulk.error} title="Could not run the action" /> : null}
|
|
147
|
+
{!pending && !editing && rowAction.error ? <ApiError error={rowAction.error} title="Could not run the action" /> : null}
|
|
148
|
+
<div className="overflow-x-auto rounded-md border">
|
|
149
|
+
<table className="w-full text-sm">
|
|
150
|
+
<thead className="text-muted-foreground text-left text-xs">
|
|
151
|
+
<tr>
|
|
152
|
+
<th className="w-8 p-2"><input type="checkbox" aria-label="Select all shown pages" checked={allShown} onChange={() => setSelected(allShown ? new Set() : new Set(shown.map((p) => p.id)))} /></th>
|
|
153
|
+
<th className="p-2">Page</th><th className="p-2">Status</th><th className="p-2">Changes</th><th className="p-2">Search score</th><th className="p-2">Rendered</th><th className="p-2 text-right">Actions</th>
|
|
154
|
+
</tr>
|
|
155
|
+
</thead>
|
|
156
|
+
<tbody>
|
|
157
|
+
{shown.length === 0 ? <tr><td colSpan={7} className="text-muted-foreground p-4 text-center">No loaded page matches.</td></tr> : shown.map((p) => (
|
|
158
|
+
<tr key={p.id} className="border-t align-top">
|
|
159
|
+
<td className="p-2"><input type="checkbox" aria-label={`Select ${p.slug ?? "page"}`} checked={selected.has(p.id)} onChange={() => toggle(p.id)} /></td>
|
|
160
|
+
<td className="p-2">
|
|
161
|
+
<span className="block font-medium">{p.slug || <EmptyValue />}</span>
|
|
162
|
+
{p.published_url ? <a className="text-muted-foreground inline-flex items-center gap-1 text-xs underline" href={p.published_url} target="_blank" rel="noopener noreferrer">Open live page<ExternalLink className="size-3" /></a> : null}
|
|
163
|
+
{p.error_message ? <span className="text-error block text-xs">{p.error_message}</span> : null}
|
|
164
|
+
</td>
|
|
165
|
+
<td className="p-2"><StatusPill tone={tone(p.status)}>{p.status ? wording(p.status) : "Unknown"}</StatusPill></td>
|
|
166
|
+
<td className="p-2">{p.change_state && p.change_state !== "no_changes" ? wording(p.change_state) : <EmptyValue />}</td>
|
|
167
|
+
<td className="p-2">{p.seo_health?.score == null ? <EmptyValue /> : p.seo_health.score}</td>
|
|
168
|
+
<td className="p-2">{shortDate(p.last_generated_at) || <EmptyValue />}</td>
|
|
169
|
+
<td className="p-2"><div className="flex flex-wrap justify-end gap-1">
|
|
170
|
+
{(["mark_ready_to_deploy", "unpublish", "mark_for_regeneration", "override", "reset_override"] as PageActionName[]).map((a) => {
|
|
171
|
+
const why = pageActionBlock(p, a);
|
|
172
|
+
return <Button key={a} size="sm" variant="ghost" aria-label={`${LABELS[a]} ${p.slug ?? "page"}`} title={why || undefined} disabled={Boolean(why) || busy === p.id} loading={busy === p.id && a !== "unpublish" && a !== "override"} onClick={() => run(p, a)}>{LABELS[a]}</Button>;
|
|
173
|
+
})}
|
|
174
|
+
</div></td>
|
|
175
|
+
</tr>
|
|
176
|
+
))}
|
|
177
|
+
</tbody>
|
|
178
|
+
</table>
|
|
179
|
+
</div>
|
|
180
|
+
<LoadMore shown={pages.length} noun="pages" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
181
|
+
<FormSheet open={editing !== null} onOpenChange={(o) => { if (!o) setEditing(null); }} title={`Replace the HTML of ${editing?.slug ?? "this page"}`} description="Your HTML replaces the rendered page. The API removes scripts, forms, styles and event handlers from it. The page keeps its edit until you undo it or regenerate.">
|
|
182
|
+
<div className="grid gap-3">
|
|
183
|
+
<Textarea aria-label="Page HTML" rows={14} value={html} onChange={(e) => setHtml(e.target.value)} placeholder="Paste the complete page HTML" />
|
|
184
|
+
<p className="text-muted-foreground text-xs">The API does not return a page's current HTML, so paste the whole replacement. {isLive(editing ?? {} as GeneratedPage) ? "This page is live; the edit reaches visitors after the next publish." : ""}</p>
|
|
185
|
+
{rowAction.error ? <ApiError error={rowAction.error} title="Could not replace the HTML" /> : null}
|
|
186
|
+
<div className="flex gap-2">
|
|
187
|
+
<Button loading={rowAction.loading} disabled={html.trim() === "" || !editing} onClick={() => editing && void rowAction.mutate({ page: editing, action: "override", html })}>Replace HTML</Button>
|
|
188
|
+
<Button variant="outline" onClick={() => setEditing(null)}>Cancel</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</FormSheet>
|
|
192
|
+
<FormSheet open={urls !== null} onOpenChange={(o) => { if (!o) setUrls(null); }} title="Page addresses" description={`${plural(urls?.length ?? 0, "live address")} for the selected pages.`}>
|
|
193
|
+
{urls && urls.length === 0 ? <p className="text-muted-foreground text-sm">None of the selected pages has a live address.</p> : <Textarea aria-label="Page addresses" readOnly rows={12} value={(urls ?? []).join("\n")} />}
|
|
194
|
+
</FormSheet>
|
|
195
|
+
{confirmText ? (
|
|
196
|
+
<ConfirmSpendDialog
|
|
197
|
+
open
|
|
198
|
+
tier={confirmText.tier}
|
|
199
|
+
title={confirmText.title}
|
|
200
|
+
affected={confirmText.affected}
|
|
201
|
+
description={<><p>{confirmText.body}</p>{pending && (bulk.error || rowAction.error) ? <ApiError error={bulk.error ?? rowAction.error} title="Could not run the action" /> : null}</>}
|
|
202
|
+
confirmLabel={confirmText.label}
|
|
203
|
+
confirming={confirming}
|
|
204
|
+
onCancel={() => setPending(null)}
|
|
205
|
+
onConfirm={() => (pending?.kind === "page" ? (setBusy(pending.page.id), void rowAction.mutate({ page: pending.page, action: "unpublish" })) : pending && void bulk.mutate({ action: pending.action, ids: pending.ids }))}
|
|
206
|
+
/>
|
|
207
|
+
) : null}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticOverview: where a campaign stands: readiness checks, the numbers, the template, search health and what the next
|
|
5
|
+
* render and publish will do. Hosts the Generate and Publish panels.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getProgrammaticWorkspace (GET /programmatic-campaigns/{id}/workspace) read readiness, counts, search, links, deployment
|
|
9
|
+
* 2. getLandingPage (GET /landing-pages/{landing_page_id}) read the template page's name, only when the workspace names it
|
|
10
|
+
* Generating and publishing live in ProgrammaticGenerate and ProgrammaticPublish, which confirm what reaches the live site.
|
|
11
|
+
* A missing number is shown as a dash, never as 0.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { CheckCircle2, Circle, ExternalLink } from "lucide-react";
|
|
15
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { asRec, shortDate, text } from "@/g8/pages/lib/studio";
|
|
21
|
+
import { useProgrammaticWorkspace } from "../hooks/use-programmatic-workspace";
|
|
22
|
+
import type { Campaign } from "../lib/programmatic-types";
|
|
23
|
+
import { wording } from "../lib/programmatic-types";
|
|
24
|
+
import { ProgrammaticGenerate } from "./programmatic-generate";
|
|
25
|
+
import { ProgrammaticPublish } from "./programmatic-publish";
|
|
26
|
+
|
|
27
|
+
export interface ProgrammaticOverviewProps {
|
|
28
|
+
campaign: Campaign;
|
|
29
|
+
/** Where the template page is edited. Default: the pages screen at /pages?id=. */
|
|
30
|
+
pagesHref?: (templatePageId: string) => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const n = (v: number | null | undefined): string | null => (v == null ? null : v.toLocaleString());
|
|
34
|
+
|
|
35
|
+
export function ProgrammaticOverview({ campaign, pagesHref = (id) => `/pages?id=${encodeURIComponent(id)}` }: ProgrammaticOverviewProps) {
|
|
36
|
+
const ws = useProgrammaticWorkspace(campaign.id);
|
|
37
|
+
const templateId = ws.data?.template?.template_page_id ?? campaign.template_page_id ?? "";
|
|
38
|
+
const template = useG8Query(
|
|
39
|
+
(signal) => g8Get(`/landing-pages/${encodeURIComponent(templateId)}`, undefined, { signal }).then((r) => asRec(r.data)),
|
|
40
|
+
[templateId], { enabled: Boolean(templateId), key: ["programmatic", "template", templateId] },
|
|
41
|
+
);
|
|
42
|
+
if (ws.error && !ws.data) return <ApiError error={ws.error} title="Could not load the campaign" onRetry={() => void ws.refetch()} />;
|
|
43
|
+
if (!ws.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
|
|
44
|
+
const o = ws.data.overview ?? {};
|
|
45
|
+
const seo = ws.data.seo;
|
|
46
|
+
const issues = Object.entries(seo?.issues ?? {}).filter(([, c]) => c > 0);
|
|
47
|
+
const name = text(template.data?.name);
|
|
48
|
+
return (
|
|
49
|
+
<div className="grid gap-4">
|
|
50
|
+
<ul aria-label="Readiness" className="grid gap-1 text-sm">
|
|
51
|
+
{(o.readiness ?? []).map((r) => (
|
|
52
|
+
<li key={r.key} className="flex items-center gap-2">
|
|
53
|
+
{r.complete ? <CheckCircle2 className="size-4" aria-label="Done" /> : <Circle className="text-muted-foreground size-4" aria-label="Not done" />}
|
|
54
|
+
<span>{r.label}</span>
|
|
55
|
+
{!r.complete && r.details?.length ? <span className="text-muted-foreground text-xs">Missing: {r.details.join(", ")}</span> : null}
|
|
56
|
+
</li>
|
|
57
|
+
))}
|
|
58
|
+
</ul>
|
|
59
|
+
<KeyValueList columns={2} items={[
|
|
60
|
+
{ label: "Source rows", value: n(o.total_rows) },
|
|
61
|
+
{ label: "Pages rendered", value: n(o.generated_pages) },
|
|
62
|
+
{ label: "Live", value: n(o.live_pages) },
|
|
63
|
+
{ label: "Failed", value: n(o.failed_pages) },
|
|
64
|
+
{ label: "Changed since render", value: n(o.changed_rows) },
|
|
65
|
+
{ label: "Hand-edited", value: n(o.manual_overrides) },
|
|
66
|
+
{ label: "Last render", value: shortDate(o.last_generation) || null },
|
|
67
|
+
{ label: "Last publish", value: shortDate(o.last_publish) || null },
|
|
68
|
+
{ label: "Next automatic render", value: o.next_regeneration || null },
|
|
69
|
+
{ label: "Search score (average)", value: seo?.average_score == null ? null : String(seo.average_score) },
|
|
70
|
+
{ label: "Search issues", value: issues.length ? issues.map(([k, c]) => `${wording(k)}: ${c}`).join(", ") : null, wide: true },
|
|
71
|
+
{ label: "Pages with no links in", value: n(ws.data.links?.orphan_pages?.length) },
|
|
72
|
+
{ label: "Address pattern", value: ws.data.template?.slug_template ?? campaign.slug_template ?? null, mono: true },
|
|
73
|
+
{
|
|
74
|
+
label: "Template page",
|
|
75
|
+
wide: true,
|
|
76
|
+
value: templateId ? <a className="inline-flex items-center gap-1 underline" href={pagesHref(templateId)}>{name || "Open the template page"}<ExternalLink className="size-3" /></a> : null,
|
|
77
|
+
},
|
|
78
|
+
]} />
|
|
79
|
+
{campaign.error_message ? <p role="alert" className="text-error text-sm">{campaign.error_message}</p> : null}
|
|
80
|
+
<ProgrammaticGenerate campaign={campaign} />
|
|
81
|
+
<ProgrammaticPublish campaign={campaign} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|