@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,221 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticPagesScreen: the finished workspace for programmatic pages: one template landing page plus a data source makes one
|
|
5
|
+
* page per row (a page per account, per city, per integration). No props needed.
|
|
6
|
+
*
|
|
7
|
+
* Composes: ScreenLayout (title, stats), Toolbar (search, status and source filters), SplitView + DetailPanel (the selected
|
|
8
|
+
* campaign, deep link `?id=`, detail tab `?tab=`), FormSheet (new campaign via ProgrammaticCampaignForm), ProgrammaticOverview
|
|
9
|
+
* (with ProgrammaticGenerate and ProgrammaticPublish), ProgrammaticGeneratedPages, ProgrammaticPreview, ProgrammaticVariables,
|
|
10
|
+
* ProgrammaticSource, ProgrammaticSettings and the form again in edit mode.
|
|
11
|
+
*
|
|
12
|
+
* User flows: see every campaign with its state, source and page counts (search; filter by state and source); create one from a
|
|
13
|
+
* template page and a list or table; open it to see its readiness and numbers; render all pages (free; confirmed when the approval
|
|
14
|
+
* mode publishes automatically) and follow the run; read, search and filter the generated pages, replace one page's HTML, mark pages
|
|
15
|
+
* ready, regenerate them, take them off the live site (confirmed), delete them (confirmed), export their addresses, send live ones to
|
|
16
|
+
* Search Console (confirmed); publish waiting pages, republish everything or roll the last publish back (each confirmed with its
|
|
17
|
+
* page count); preview one row; set per-variable fallbacks and limits; switch or import the source (a sitemap); set regeneration,
|
|
18
|
+
* publishing, search and link settings; edit the name and address pattern; delete a campaign (confirmed). The template page is edited
|
|
19
|
+
* in the pages screen, linked from the Overview.
|
|
20
|
+
*
|
|
21
|
+
* SDK operations: listProgrammaticCampaigns, deleteProgrammaticCampaign (destructive, confirmed); everything else lives in the
|
|
22
|
+
* composed parts, which confirm every action that changes the live site.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useState } from "react";
|
|
26
|
+
import type { ReactNode } from "react";
|
|
27
|
+
import { Layers, Plus, Trash2 } from "lucide-react";
|
|
28
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
29
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
30
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
31
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
32
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
33
|
+
import { SelectFilter } from "@/g8/components/screen/filter-controls";
|
|
34
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
35
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
36
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
37
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
38
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
39
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
40
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
41
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
42
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
43
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
44
|
+
import { plural } from "@/g8/lib/plural";
|
|
45
|
+
import { shortDate } from "@/g8/pages/lib/studio";
|
|
46
|
+
import { CAMPAIGN_STATUSES, SOURCE_TYPES, campaignRows, sourceOf, wording } from "../lib/programmatic-types";
|
|
47
|
+
import type { Campaign } from "../lib/programmatic-types";
|
|
48
|
+
import { ProgrammaticCampaignForm } from "./programmatic-campaign-form";
|
|
49
|
+
import { ProgrammaticGeneratedPages } from "./programmatic-generated-pages";
|
|
50
|
+
import { ProgrammaticOverview } from "./programmatic-overview";
|
|
51
|
+
import { ProgrammaticPreview } from "./programmatic-preview";
|
|
52
|
+
import { ProgrammaticSettings } from "./programmatic-settings";
|
|
53
|
+
import { ProgrammaticSource } from "./programmatic-source";
|
|
54
|
+
import { ProgrammaticVariables } from "./programmatic-variables";
|
|
55
|
+
|
|
56
|
+
export interface ProgrammaticPagesScreenProps {
|
|
57
|
+
/** Where a campaign's template page is edited. Default: /pages?id=<id> (the pages screen). */
|
|
58
|
+
pagesHref?: (templatePageId: string) => string;
|
|
59
|
+
renderRowActions?: (campaign: Campaign) => ReactNode;
|
|
60
|
+
renderDetailActions?: (campaign: Campaign) => ReactNode;
|
|
61
|
+
extraDetailTabs?: { id: string; label: string; render: (campaign: Campaign) => ReactNode }[];
|
|
62
|
+
toolbarActions?: ReactNode;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tone = (s?: string | null) => (s === "completed" ? "positive" : s === "failed" ? "negative" : s === "generating" || s === "validating" ? "info" : "neutral");
|
|
67
|
+
const sourceLabel = (c: Campaign): string => SOURCE_TYPES.find((t) => t.value === sourceOf(c).type)?.label ?? "";
|
|
68
|
+
const sum = (rows: Campaign[], pick: (c: Campaign) => number | null | undefined): number | null => {
|
|
69
|
+
const known = rows.map(pick).filter((v): v is number => typeof v === "number");
|
|
70
|
+
return known.length ? known.reduce((a, b) => a + b, 0) : null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function ProgrammaticPagesScreen({ pagesHref, renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: ProgrammaticPagesScreenProps) {
|
|
74
|
+
const [id, setId] = useUrlState("id", "");
|
|
75
|
+
const [view, setView] = useUrlState("view", "");
|
|
76
|
+
const [tab, setTab] = useUrlState("tab", "overview");
|
|
77
|
+
const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
|
|
78
|
+
const [search, setSearch] = useState("");
|
|
79
|
+
const [status, setStatus] = useState("");
|
|
80
|
+
const [source, setSource] = useState("");
|
|
81
|
+
const [creating, setCreating] = useState(false);
|
|
82
|
+
const [toDelete, setToDelete] = useState<Campaign | null>(null);
|
|
83
|
+
|
|
84
|
+
const list = useG8Query((signal) => g8Get("/programmatic-campaigns", {}, { signal }).then((r) => campaignRows(r.data)), [], { key: ["programmatic", "list"] });
|
|
85
|
+
const remove = useG8Mutation((c: Campaign) => g8Delete(`/programmatic-campaigns/${encodeURIComponent(c.id)}`), {
|
|
86
|
+
onSuccess: (_r, c) => { setToDelete(null); if (c.id === id) { setId(null); setView(null); } },
|
|
87
|
+
invalidates: [["programmatic"]],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const all = list.data ?? [];
|
|
91
|
+
const q = search.trim().toLowerCase();
|
|
92
|
+
const shown = all.filter((c) => (!status || c.status === status) && (!source || sourceOf(c).type === source) && (!q || `${c.name} ${c.slug_template ?? ""}`.toLowerCase().includes(q)));
|
|
93
|
+
const selected = all.find((c) => c.id === id);
|
|
94
|
+
const closeDetail = (): void => { setId(null); setView(null); setTab(null); };
|
|
95
|
+
const rendered = sum(all, (c) => c.pages_generated);
|
|
96
|
+
const failed = sum(all, (c) => c.pages_failed);
|
|
97
|
+
|
|
98
|
+
const rows = list.error ? <ApiError error={list.error} title="Could not load programmatic campaigns" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? (
|
|
99
|
+
<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>
|
|
100
|
+
) : shown.length === 0 ? (
|
|
101
|
+
<EmptyState icon={<Layers className="size-8" />} title={all.length === 0 ? "No programmatic campaigns yet" : "No campaigns match"} description={all.length === 0 ? "Pick a template landing page and a list or table. One page is made for every row: a page per account, per city, per integration." : "Try another search or filter."} action={all.length === 0 ? <Button onClick={() => setCreating(true)}>Create your first campaign</Button> : undefined} />
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
<p role="status" className="text-muted-foreground mb-2 text-xs">{shown.length === all.length ? `All ${plural(all.length, "campaign")}` : `${shown.length.toLocaleString()} of ${plural(all.length, "campaign")}`}</p>
|
|
105
|
+
<ul className="grid gap-2">
|
|
106
|
+
{shown.map((c) => (
|
|
107
|
+
<li key={c.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${c.id === id ? "bg-muted" : ""}`}>
|
|
108
|
+
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(c.id)}>
|
|
109
|
+
<span className="block truncate font-medium">{c.name}</span>
|
|
110
|
+
<span className="text-muted-foreground block truncate text-xs">{sourceLabel(c) || "Source unknown"} · {c.pages_generated == null ? "-" : c.pages_generated.toLocaleString()} of {c.total_pages == null ? "-" : c.total_pages.toLocaleString()} pages · {shortDate(c.last_generated_at) || "not rendered"}</span>
|
|
111
|
+
</button>
|
|
112
|
+
<StatusPill tone={tone(c.status)}>{c.status ? wording(c.status) : "Unknown"}</StatusPill>
|
|
113
|
+
<div className="flex gap-1">
|
|
114
|
+
<Button size="sm" variant="ghost" aria-label={`Delete ${c.name}`} onClick={() => { remove.reset(); setToDelete(c); }}><Trash2 className="size-4" /></Button>
|
|
115
|
+
{renderRowActions?.(c)}
|
|
116
|
+
</div>
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const detail = selected ? (
|
|
124
|
+
<DetailPanel
|
|
125
|
+
title={selected.name}
|
|
126
|
+
subtitle={selected.slug_template ?? undefined}
|
|
127
|
+
badges={<StatusPill tone={tone(selected.status)}>{selected.status ? wording(selected.status) : "Unknown"}</StatusPill>}
|
|
128
|
+
actions={renderDetailActions?.(selected)}
|
|
129
|
+
onClose={closeDetail}
|
|
130
|
+
{...expandProps}
|
|
131
|
+
activeTab={tab}
|
|
132
|
+
onTabChange={(t) => setTab(t === "overview" ? null : t)}
|
|
133
|
+
tabs={[
|
|
134
|
+
{ id: "overview", label: "Overview", content: <ProgrammaticOverview key={selected.id} campaign={selected} pagesHref={pagesHref} /> },
|
|
135
|
+
{ id: "pages", label: "Pages", content: <ProgrammaticGeneratedPages key={selected.id} campaign={selected} /> },
|
|
136
|
+
{ id: "preview", label: "Preview", content: <ProgrammaticPreview key={selected.id} campaign={selected} /> },
|
|
137
|
+
{ id: "variables", label: "Variables", content: <ProgrammaticVariables key={selected.id} campaign={selected} /> },
|
|
138
|
+
{ id: "source", label: "Source", content: <ProgrammaticSource key={selected.id} campaign={selected} /> },
|
|
139
|
+
{ id: "settings", label: "Settings", content: <ProgrammaticSettings key={selected.id} campaign={selected} /> },
|
|
140
|
+
{ id: "edit", label: "Edit", content: <ProgrammaticCampaignForm key={selected.id} campaign={selected} onSaved={() => void list.refetch()} /> },
|
|
141
|
+
{ id: "details", label: "Details", content: <CampaignDetails campaign={selected} /> },
|
|
142
|
+
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(selected) })),
|
|
143
|
+
]}
|
|
144
|
+
/>
|
|
145
|
+
) : null;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<ScreenLayout
|
|
149
|
+
className={className}
|
|
150
|
+
title="Programmatic pages"
|
|
151
|
+
description="One template page and one data source make a page for every row: a page per account, per city, per integration."
|
|
152
|
+
icon={<Layers className="size-5" />}
|
|
153
|
+
actions={<Button size="sm" onClick={() => setCreating(true)}><Plus className="size-4" />New campaign</Button>}
|
|
154
|
+
stats={[
|
|
155
|
+
{ label: "Campaigns", value: list.data ? all.length.toLocaleString() : "..." },
|
|
156
|
+
{ label: "Pages rendered", value: list.data ? (rendered == null ? "-" : rendered.toLocaleString()) : "..." },
|
|
157
|
+
{ label: "Failed pages", value: list.data ? (failed == null ? "-" : failed.toLocaleString()) : "...", tone: failed ? "negative" : undefined },
|
|
158
|
+
{ label: "Rendering now", value: list.data ? all.filter((c) => c.status === "generating").length.toLocaleString() : "...", tone: "positive" },
|
|
159
|
+
]}
|
|
160
|
+
>
|
|
161
|
+
<SplitView
|
|
162
|
+
list={(
|
|
163
|
+
<div className="grid gap-3">
|
|
164
|
+
<Toolbar
|
|
165
|
+
search={{ value: search, onChange: setSearch, placeholder: "Search campaigns" }}
|
|
166
|
+
filters={(<>
|
|
167
|
+
<SelectFilter label="state" value={status} onChange={setStatus} any="Any state" options={CAMPAIGN_STATUSES.map((s) => ({ value: s, label: wording(s) }))} />
|
|
168
|
+
<SelectFilter label="source" value={source} onChange={setSource} any="Any source" options={SOURCE_TYPES.map((t) => ({ value: t.value, label: t.label }))} />
|
|
169
|
+
</>)}
|
|
170
|
+
actions={toolbarActions}
|
|
171
|
+
/>
|
|
172
|
+
{rows}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
detail={detail}
|
|
176
|
+
detailOpen={Boolean(selected)}
|
|
177
|
+
onCloseDetail={closeDetail}
|
|
178
|
+
expanded={view === "full"}
|
|
179
|
+
onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
180
|
+
detailWidth="xl"
|
|
181
|
+
/>
|
|
182
|
+
<FormSheet open={creating} onOpenChange={setCreating} title="New programmatic campaign" description="Nothing is rendered or published until you ask.">
|
|
183
|
+
{creating ? <ProgrammaticCampaignForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); void list.refetch(); setTab(null); setId(created.id); }} /> : null}
|
|
184
|
+
</FormSheet>
|
|
185
|
+
<ConfirmSpendDialog
|
|
186
|
+
open={toDelete !== null}
|
|
187
|
+
tier="destructive"
|
|
188
|
+
title={`Delete ${toDelete?.name ?? "this campaign"} and at least its ${plural(toDelete ? (toDelete.pages_generated ?? 0) + (toDelete.pages_failed ?? 0) : 0, "page")}?`}
|
|
189
|
+
affected={toDelete ? (toDelete.pages_generated ?? 0) + (toDelete.pages_failed ?? 0) : undefined}
|
|
190
|
+
description={<><p>The campaign and every page it generated are removed. The count is at least this many: a re-run keeps earlier pages, so more can go. On a hosted address a live page stops being served as soon as it is deleted; on a custom domain its last published copy may stay up until the next publish. Unpublish first if you only want pages offline.</p>{remove.error ? <ApiError error={remove.error} title="Could not delete the campaign" /> : null}</>}
|
|
191
|
+
confirmLabel="Delete"
|
|
192
|
+
confirming={remove.loading}
|
|
193
|
+
onCancel={() => setToDelete(null)}
|
|
194
|
+
onConfirm={() => { if (toDelete) void remove.mutate(toDelete); }}
|
|
195
|
+
/>
|
|
196
|
+
</ScreenLayout>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function CampaignDetails({ campaign }: { campaign: Campaign }) {
|
|
201
|
+
const src = sourceOf(campaign);
|
|
202
|
+
return (
|
|
203
|
+
<KeyValueList
|
|
204
|
+
columns={2}
|
|
205
|
+
items={[
|
|
206
|
+
{ label: "Name", value: campaign.name },
|
|
207
|
+
{ label: "State", value: campaign.status ? wording(campaign.status) : null },
|
|
208
|
+
{ label: "Lifecycle", value: campaign.lifecycle_state ? wording(campaign.lifecycle_state) : null },
|
|
209
|
+
{ label: "Changes", value: campaign.change_state && campaign.change_state !== "no_changes" ? wording(campaign.change_state) : null },
|
|
210
|
+
{ label: "Source", value: sourceLabel(campaign) || null },
|
|
211
|
+
{ label: "Source name", value: src.name || null },
|
|
212
|
+
{ label: "Address pattern", value: campaign.slug_template ?? null, mono: true, wide: true },
|
|
213
|
+
{ label: "Pages rendered", value: campaign.pages_generated == null ? null : campaign.pages_generated.toLocaleString() },
|
|
214
|
+
{ label: "Pages failed", value: campaign.pages_failed == null ? null : campaign.pages_failed.toLocaleString() },
|
|
215
|
+
{ label: "Created", value: shortDate(campaign.created_at) || null },
|
|
216
|
+
{ label: "Updated", value: shortDate(campaign.updated_at) || null },
|
|
217
|
+
{ label: "Campaign id", value: campaign.id, mono: true },
|
|
218
|
+
]}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticPreview: see one page rendered for one source row, without saving anything.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getProgrammaticWorkspace (GET /programmatic-campaigns/{id}/workspace) read the first sample rows of the source
|
|
8
|
+
* 2. previewProgrammaticPage (POST /programmatic-campaigns/{id}/preview) write renders one row; FREE, saves nothing
|
|
9
|
+
* A row is chosen from the sample rows the workspace returns (only rows with a numeric id can be asked for by id); "First row" sends
|
|
10
|
+
* no row at all and the API uses the source's first row. The HTML is shown in a sandboxed frame with scripts off.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Eye } from "lucide-react";
|
|
15
|
+
import { Button, Label } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyValue, KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
20
|
+
import { text } from "@/g8/pages/lib/studio";
|
|
21
|
+
import type { Rec } from "@/g8/pages/lib/studio";
|
|
22
|
+
import { useProgrammaticWorkspace } from "../hooks/use-programmatic-workspace";
|
|
23
|
+
import type { Campaign } from "../lib/programmatic-types";
|
|
24
|
+
|
|
25
|
+
export interface ProgrammaticPreviewProps {
|
|
26
|
+
campaign: Campaign;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Rendered { html?: string; slug?: string; contact_id?: number | null; contact_name?: string }
|
|
30
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
31
|
+
|
|
32
|
+
/** A readable name for a source row: a person, company or title field, else its position. */
|
|
33
|
+
export function rowLabel(row: Rec, index: number): string {
|
|
34
|
+
const person = [text(row.first_name), text(row.last_name)].filter(Boolean).join(" ");
|
|
35
|
+
return person || text(row.name) || text(row.company) || text(row.company_name) || text(row.title) || text(row.email) || `Row ${index + 1}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ProgrammaticPreview({ campaign }: ProgrammaticPreviewProps) {
|
|
39
|
+
const ws = useProgrammaticWorkspace(campaign.id);
|
|
40
|
+
const [rowId, setRowId] = useState("");
|
|
41
|
+
const rows = (ws.data?.data?.sample_rows ?? []).map((r, i) => ({ id: typeof r.id === "number" ? r.id : null, label: rowLabel(r, i) })).filter((r): r is { id: number; label: string } => r.id !== null);
|
|
42
|
+
const render = useG8Mutation((id: string) => g8Post<Rendered>(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}/preview`, id ? { contact_id: Number(id) } : {}));
|
|
43
|
+
const out = render.data?.data;
|
|
44
|
+
return (
|
|
45
|
+
<section aria-label="Preview" className="grid gap-3">
|
|
46
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
47
|
+
<div>
|
|
48
|
+
<Label htmlFor="g8-pp-prev-row" className="mb-1">Source row</Label>
|
|
49
|
+
<select id="g8-pp-prev-row" className={SELECT} value={rowId} onChange={(e) => setRowId(e.target.value)}>
|
|
50
|
+
<option value="">First row</option>
|
|
51
|
+
{rows.map((r) => <option key={r.id} value={r.id}>{r.label}</option>)}
|
|
52
|
+
</select>
|
|
53
|
+
</div>
|
|
54
|
+
<Button size="sm" loading={render.loading} onClick={() => void render.mutate(rowId)}><Eye className="size-4" />Render preview</Button>
|
|
55
|
+
</div>
|
|
56
|
+
{ws.error ? <ApiError error={ws.error} title="Could not load the source rows" onRetry={() => void ws.refetch()} /> : null}
|
|
57
|
+
{render.error ? <ApiError error={render.error} title="Could not render the preview" /> : null}
|
|
58
|
+
{out ? (
|
|
59
|
+
<>
|
|
60
|
+
<KeyValueList columns={2} items={[{ label: "Row", value: out.contact_name || null }, { label: "Page address", value: out.slug || null, mono: true }]} />
|
|
61
|
+
{out.html ? <iframe title="Page preview" sandbox="" srcDoc={out.html} className="bg-background h-[28rem] w-full rounded-md border" /> : <p className="text-muted-foreground text-sm">The API returned no HTML <EmptyValue /></p>}
|
|
62
|
+
</>
|
|
63
|
+
) : <p className="text-muted-foreground text-sm">Pick a row and render it to see the finished page. Nothing is saved.</p>}
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticPublish: put the campaign's pages on the live site, republish them, or roll the last publish back.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getProgrammaticWorkspace (GET /programmatic-campaigns/{id}/workspace) read pages waiting, pages live, publish history
|
|
8
|
+
* 2. runProgrammaticBulkPageAction (POST /programmatic-campaigns/{id}/pages/actions) external deploy_pending_changes, republish_all,
|
|
9
|
+
* rollback_publish. Each changes what real visitors see, so each is confirmed with the number of pages it touches.
|
|
10
|
+
* The numbers in the confirm come from the workspace counts: pages waiting (rendered, not live), pages live, and what a
|
|
11
|
+
* republish covers (every rendered page that did not fail, drafts included). Publishing needs a connected Cloudflare site or the
|
|
12
|
+
* workspace's hosting; when neither is configured the API says so and the message is shown. Nothing is sent without the confirm.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Rocket, RotateCcw } from "lucide-react";
|
|
17
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { EmptyValue, KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
21
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { invalidate } from "@/g8/lib/g8-cache";
|
|
24
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { plural } from "@/g8/lib/plural";
|
|
26
|
+
import { asRec, num, shortDate, text } from "@/g8/pages/lib/studio";
|
|
27
|
+
import { useProgrammaticWorkspace } from "../hooks/use-programmatic-workspace";
|
|
28
|
+
import type { Campaign, DeployActionName } from "../lib/programmatic-types";
|
|
29
|
+
|
|
30
|
+
export interface ProgrammaticPublishProps {
|
|
31
|
+
campaign: Campaign;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ProgrammaticPublish({ campaign }: ProgrammaticPublishProps) {
|
|
35
|
+
const ws = useProgrammaticWorkspace(campaign.id);
|
|
36
|
+
const [asking, setAsking] = useState<DeployActionName | null>(null);
|
|
37
|
+
const [done, setDone] = useState("");
|
|
38
|
+
const run = useG8Mutation((action: DeployActionName) => g8Post(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}/pages/actions`, { action }), {
|
|
39
|
+
onSuccess: (res, action) => {
|
|
40
|
+
const d = asRec(res.data);
|
|
41
|
+
setAsking(null);
|
|
42
|
+
setDone(`${action === "rollback_publish" ? "Rolled back" : "Published"} ${plural(num(d.count) ?? 0, "page")}.${text(d.deployment_url) ? ` Site: ${text(d.deployment_url)}` : ""}`);
|
|
43
|
+
void invalidate(["programmatic"]);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (ws.error && !ws.data) return <ApiError error={ws.error} title="Could not load the publishing state" onRetry={() => void ws.refetch()} />;
|
|
47
|
+
if (!ws.data) return <Skeleton className="h-24 w-full" aria-busy="true" />;
|
|
48
|
+
|
|
49
|
+
const counts = ws.data.pages?.status_counts ?? {};
|
|
50
|
+
const waiting = ws.data.deployment?.pending_pages ?? counts.generated ?? null;
|
|
51
|
+
const live = ws.data.deployment?.published_pages ?? counts.published ?? null;
|
|
52
|
+
const covered = (counts.generated ?? 0) + (counts.published ?? 0);
|
|
53
|
+
const changed = ws.data.overview?.changed_rows ?? null;
|
|
54
|
+
const last = ws.data.deployment?.diagnostics;
|
|
55
|
+
const history = ws.data.deployment?.history ?? [];
|
|
56
|
+
const canRollback = Boolean(ws.data.campaign?.deployment_run_id ?? campaign.deployment_run_id);
|
|
57
|
+
const nothingWaiting = (waiting ?? 0) === 0 && (changed ?? 0) === 0;
|
|
58
|
+
const copy: Record<DeployActionName, { title: string; body: string; affected: number | undefined; label: string }> = {
|
|
59
|
+
deploy_pending_changes: { title: `Publish ${waiting == null ? "the waiting pages" : plural(waiting, "waiting page")}?`, body: `Puts the rendered pages that are not live yet on the live site, and refreshes live pages whose data or template changed${changed == null ? "" : ` (${plural(changed, "changed row")})`}.`, affected: waiting ?? undefined, label: "Publish" },
|
|
60
|
+
republish_all: { title: `Republish ${plural(covered, "page")}?`, body: "Rebuilds the whole site from every rendered page that did not fail. Rendered drafts that are not live yet go live too.", affected: covered, label: "Republish" },
|
|
61
|
+
rollback_publish: { title: "Roll back the last publish?", body: "Takes the pages from the most recent publish off the live site and returns them to ready. Earlier publishes are not touched.", affected: undefined, label: "Roll back" },
|
|
62
|
+
};
|
|
63
|
+
const open = asking ? copy[asking] : null;
|
|
64
|
+
return (
|
|
65
|
+
<section aria-label="Publish" className="grid gap-3 rounded-md border p-3">
|
|
66
|
+
<div>
|
|
67
|
+
<h3 className="text-sm font-medium">Publish to the live site</h3>
|
|
68
|
+
<p className="text-muted-foreground text-xs">Rendering pages never publishes them (unless the approval mode says so). These actions do.</p>
|
|
69
|
+
</div>
|
|
70
|
+
<KeyValueList columns={2} items={[
|
|
71
|
+
{ label: "Waiting to go live", value: waiting == null ? null : waiting.toLocaleString() },
|
|
72
|
+
{ label: "Live now", value: live == null ? null : live.toLocaleString() },
|
|
73
|
+
{ label: "Last publish", value: shortDate(campaign.last_deployed_at ?? ws.data.overview?.last_publish) || null },
|
|
74
|
+
{ label: "Address pattern", value: ws.data.deployment?.url_pattern_preview ?? null, mono: true },
|
|
75
|
+
]} />
|
|
76
|
+
<div className="flex flex-wrap gap-2">
|
|
77
|
+
<Button size="sm" disabled={nothingWaiting} title={nothingWaiting ? "No page is waiting to go live and no live page has changed." : undefined} onClick={() => { run.reset(); setAsking("deploy_pending_changes"); }}><Rocket className="size-4" />Publish waiting pages</Button>
|
|
78
|
+
<Button size="sm" variant="outline" disabled={covered === 0} title={covered === 0 ? "No rendered page to republish." : undefined} onClick={() => { run.reset(); setAsking("republish_all"); }}>Republish everything</Button>
|
|
79
|
+
<Button size="sm" variant="outline" disabled={!canRollback} title={canRollback ? undefined : "There is no publish to roll back."} onClick={() => { run.reset(); setAsking("rollback_publish"); }}><RotateCcw className="size-4" />Roll back last publish</Button>
|
|
80
|
+
</div>
|
|
81
|
+
{done ? <p role="status" className="text-xs">{done}</p> : null}
|
|
82
|
+
{last?.status ? <p className="text-xs">Last result: <StatusPill tone={last.status === "ok" ? "positive" : last.status === "error" ? "negative" : "neutral"}>{last.status}</StatusPill> {last.message || <EmptyValue />}</p> : null}
|
|
83
|
+
{history.length > 0 ? (
|
|
84
|
+
<ul aria-label="Publish history" className="text-muted-foreground grid gap-1 text-xs">
|
|
85
|
+
{history.slice(0, 5).map((h, i) => <li key={`${h.checked_at ?? i}`}>{shortDate(h.checked_at) || <EmptyValue />}: {h.message || <EmptyValue />}</li>)}
|
|
86
|
+
</ul>
|
|
87
|
+
) : null}
|
|
88
|
+
{!asking && run.error ? <ApiError error={run.error} title="Could not run the action" /> : null}
|
|
89
|
+
<ConfirmSpendDialog
|
|
90
|
+
open={asking !== null}
|
|
91
|
+
tier="external"
|
|
92
|
+
title={open?.title ?? "Publish"}
|
|
93
|
+
affected={open?.affected}
|
|
94
|
+
description={<><p>{open?.body}</p>{run.error ? <ApiError error={run.error} title="Could not run the action" /> : null}</>}
|
|
95
|
+
confirmLabel={open?.label ?? "Publish"}
|
|
96
|
+
confirming={run.loading}
|
|
97
|
+
onCancel={() => setAsking(null)}
|
|
98
|
+
onConfirm={() => { if (asking) void run.mutate(asking); }}
|
|
99
|
+
/>
|
|
100
|
+
</section>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticSettings: how a campaign regenerates, where its pages are published, and its search and link rules.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getProgrammaticCampaign (GET /programmatic-campaigns/{id}) read the full settings (the list row may omit them)
|
|
8
|
+
* 2. updateProgrammaticWorkspace (PATCH /programmatic-campaigns/{id}/workspace) write only the keys that changed
|
|
9
|
+
* Settings are merged by the API per group, so the save sends only the changed keys of the changed groups, and a cleared text is
|
|
10
|
+
* sent as null. Choosing "Publish automatically" is confirmed here, because it makes every later render go live. Nothing is
|
|
11
|
+
* published by saving.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useState } from "react";
|
|
15
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
20
|
+
import { g8Get, g8Patch } from "@/g8/lib/g8-client";
|
|
21
|
+
import { APPROVAL_MODES, REGENERATION_MODES, settingsDraftOf, settingsPatch } from "../lib/programmatic-types";
|
|
22
|
+
import type { Campaign, SettingsDraft } from "../lib/programmatic-types";
|
|
23
|
+
|
|
24
|
+
export interface ProgrammaticSettingsProps {
|
|
25
|
+
campaign: Campaign;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
29
|
+
|
|
30
|
+
export function ProgrammaticSettings({ campaign }: ProgrammaticSettingsProps) {
|
|
31
|
+
const full = useG8Query(
|
|
32
|
+
(signal) => g8Get<Campaign>(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}`, {}, { signal }).then((r) => r.data),
|
|
33
|
+
[campaign.id], { key: ["programmatic", "detail", campaign.id] },
|
|
34
|
+
);
|
|
35
|
+
if (full.error && !full.data) return <ApiError error={full.error} title="Could not load the settings" onRetry={() => void full.refetch()} />;
|
|
36
|
+
if (!full.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
|
|
37
|
+
return <Fields key={campaign.id} campaign={full.data} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Fields({ campaign }: { campaign: Campaign }) {
|
|
41
|
+
const was = settingsDraftOf(campaign);
|
|
42
|
+
const [d, setD] = useState<SettingsDraft>(was);
|
|
43
|
+
const [asking, setAsking] = useState(false);
|
|
44
|
+
const patch = settingsPatch(was, d);
|
|
45
|
+
const dirty = Object.keys(patch).length > 0;
|
|
46
|
+
const thinOk = d.thinWords.trim() === "" || /^\d+$/.test(d.thinWords.trim());
|
|
47
|
+
const save = useG8Mutation(() => g8Patch(`/programmatic-campaigns/${encodeURIComponent(campaign.id)}/workspace`, { settings: patch }), {
|
|
48
|
+
onSuccess: () => setAsking(false), onError: () => setAsking(false), invalidates: [["programmatic"]],
|
|
49
|
+
});
|
|
50
|
+
const set = (p: Partial<SettingsDraft>): void => setD((cur) => ({ ...cur, ...p }));
|
|
51
|
+
const turnsOnAuto = d.approval === "auto_publish" && was.approval !== "auto_publish";
|
|
52
|
+
return (
|
|
53
|
+
<form className="grid gap-4" onSubmit={(e) => { e.preventDefault(); if (!dirty || !thinOk) return; if (turnsOnAuto) { save.reset(); setAsking(true); } else void save.mutate(undefined); }}>
|
|
54
|
+
<fieldset className="grid gap-3 sm:grid-cols-2">
|
|
55
|
+
<legend className="mb-2 text-sm font-medium">Regeneration</legend>
|
|
56
|
+
<div><Label htmlFor="g8-pp-mode" className="mb-1">Render again</Label>
|
|
57
|
+
<select id="g8-pp-mode" className={SELECT} value={d.mode} onChange={(e) => set({ mode: e.target.value })}>{was.mode === "" ? <option value="">Not set</option> : null}{REGENERATION_MODES.map((m) => <option key={m.value} value={m.value}>{m.label}</option>)}</select></div>
|
|
58
|
+
<div><Label htmlFor="g8-pp-approval" className="mb-1">New pages</Label>
|
|
59
|
+
<select id="g8-pp-approval" className={SELECT} value={d.approval} onChange={(e) => set({ approval: e.target.value })}>{was.approval === "" ? <option value="">Not set</option> : null}{APPROVAL_MODES.map((m) => <option key={m.value} value={m.value}>{m.label}</option>)}</select>
|
|
60
|
+
<p className="text-muted-foreground mt-1 text-xs">{APPROVAL_MODES.find((m) => m.value === d.approval)?.hint}</p></div>
|
|
61
|
+
<label className="flex items-center gap-2 text-sm sm:col-span-2"><input type="checkbox" checked={d.changedOnly} onChange={(e) => set({ changedOnly: e.target.checked })} />Only render rows that changed</label>
|
|
62
|
+
</fieldset>
|
|
63
|
+
<fieldset className="grid gap-3 sm:grid-cols-2">
|
|
64
|
+
<legend className="mb-2 text-sm font-medium">Publishing</legend>
|
|
65
|
+
<div><Label htmlFor="g8-pp-domain" className="mb-1">Domain</Label><Input id="g8-pp-domain" placeholder="pages.example.com" value={d.domain} onChange={(e) => set({ domain: e.target.value })} /></div>
|
|
66
|
+
<div><Label htmlFor="g8-pp-base" className="mb-1">Folder</Label><Input id="g8-pp-base" placeholder="integrations" value={d.basePath} onChange={(e) => set({ basePath: e.target.value })} /></div>
|
|
67
|
+
</fieldset>
|
|
68
|
+
<fieldset className="grid gap-3 sm:grid-cols-2">
|
|
69
|
+
<legend className="mb-2 text-sm font-medium">Search</legend>
|
|
70
|
+
<div><Label htmlFor="g8-pp-thin" className="mb-1">Thin page below (words)</Label><Input id="g8-pp-thin" inputMode="numeric" value={d.thinWords} onChange={(e) => set({ thinWords: e.target.value })} aria-invalid={!thinOk} /></div>
|
|
71
|
+
<div><Label htmlFor="g8-pp-canon" className="mb-1">Canonical address pattern</Label><Input id="g8-pp-canon" value={d.canonical} onChange={(e) => set({ canonical: e.target.value })} /></div>
|
|
72
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={d.schemaRequired} onChange={(e) => set({ schemaRequired: e.target.checked })} />Flag pages with no structured data</label>
|
|
73
|
+
</fieldset>
|
|
74
|
+
<fieldset className="grid gap-2">
|
|
75
|
+
<legend className="mb-2 text-sm font-medium">Links</legend>
|
|
76
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={d.sitemap} onChange={(e) => set({ sitemap: e.target.checked })} />Include pages in a sitemap</label>
|
|
77
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={d.breadcrumbs} onChange={(e) => set({ breadcrumbs: e.target.checked })} />Breadcrumbs</label>
|
|
78
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={d.related} onChange={(e) => set({ related: e.target.checked })} />Related pages</label>
|
|
79
|
+
</fieldset>
|
|
80
|
+
{!thinOk ? <p role="alert" className="text-error text-xs">The word count must be a whole number.</p> : null}
|
|
81
|
+
{!asking && save.error ? <ApiError error={save.error} title="Could not save the settings" /> : null}
|
|
82
|
+
{save.data && !dirty ? <p role="status" className="text-xs">Settings saved.</p> : null}
|
|
83
|
+
<div><Button type="submit" loading={save.loading} disabled={!dirty || !thinOk}>Save settings</Button></div>
|
|
84
|
+
<ConfirmSpendDialog
|
|
85
|
+
open={asking}
|
|
86
|
+
tier="external"
|
|
87
|
+
title="Publish new pages automatically?"
|
|
88
|
+
description={<p>From now on every render marks its pages live as soon as they are made, with no review step. Existing pages are not changed by this save.</p>}
|
|
89
|
+
confirmLabel="Publish automatically"
|
|
90
|
+
confirming={save.loading}
|
|
91
|
+
onCancel={() => setAsking(false)}
|
|
92
|
+
onConfirm={() => void save.mutate(undefined)}
|
|
93
|
+
/>
|
|
94
|
+
</form>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProgrammaticSourceFields: choose where a campaign's rows come from (a contact list, or a Workbench / CSV / sitemap-import table).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: listWorkbenchTables (GET /workbench/tables) read, only while a table source type is chosen.
|
|
7
|
+
* The list choice is the crm block's ListSelect (GET /lists, paged). No id is typed by hand.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Label } from "@graph8/sdk/ui";
|
|
11
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
12
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
13
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
14
|
+
import { hasNextOf } from "@/g8/lib/paging";
|
|
15
|
+
import { ListSelect } from "@/g8/crm/components/list-select";
|
|
16
|
+
import { rowsOf } from "@/g8/pages/lib/studio";
|
|
17
|
+
import { SOURCE_TYPES } from "../lib/programmatic-types";
|
|
18
|
+
import type { SourceType } from "../lib/programmatic-types";
|
|
19
|
+
|
|
20
|
+
export interface SourceValue { type: SourceType; listId: number | null; tableId: string; tableName: string }
|
|
21
|
+
|
|
22
|
+
export interface ProgrammaticSourceFieldsProps {
|
|
23
|
+
value: SourceValue;
|
|
24
|
+
onChange: (next: SourceValue) => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** Name to show for the current list or table before the options have loaded. */
|
|
27
|
+
currentName?: string;
|
|
28
|
+
idPrefix?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TableRow { id?: string | null; name?: string | null; row_count?: number | null }
|
|
32
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
|
|
33
|
+
const TABLE_READ = 100;
|
|
34
|
+
|
|
35
|
+
export function ProgrammaticSourceFields({ value, onChange, disabled, currentName, idPrefix = "g8-pp" }: ProgrammaticSourceFieldsProps) {
|
|
36
|
+
const tables = useG8Query(
|
|
37
|
+
(signal) => g8Get("/workbench/tables", { limit: TABLE_READ }, { signal }).then((r) => {
|
|
38
|
+
const rows = rowsOf<TableRow>(r.data, "tables");
|
|
39
|
+
return { rows, more: hasNextOf(r, TABLE_READ, rows.length) === true };
|
|
40
|
+
}),
|
|
41
|
+
[], { enabled: value.type !== "list" },
|
|
42
|
+
);
|
|
43
|
+
const rows = (tables.data?.rows ?? []).filter((t): t is TableRow & { id: string } => Boolean(t.id));
|
|
44
|
+
const known = rows.some((t) => t.id === value.tableId);
|
|
45
|
+
return (
|
|
46
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
47
|
+
<div>
|
|
48
|
+
<Label htmlFor={`${idPrefix}-source-type`} className="mb-1">Data source</Label>
|
|
49
|
+
<select id={`${idPrefix}-source-type`} className={SELECT} value={value.type} disabled={disabled} onChange={(e) => onChange({ type: e.target.value as SourceType, listId: null, tableId: "", tableName: "" })}>
|
|
50
|
+
{SOURCE_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}
|
|
51
|
+
</select>
|
|
52
|
+
<p className="text-muted-foreground mt-1 text-xs">{SOURCE_TYPES.find((t) => t.value === value.type)?.hint}</p>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
{value.type === "list" ? (
|
|
56
|
+
<>
|
|
57
|
+
<Label htmlFor={`${idPrefix}-list`} className="mb-1">List</Label>
|
|
58
|
+
<ListSelect id={`${idPrefix}-list`} label="Source list" emptyOption="Choose a list" type="contacts" value={value.listId} valueLabel={currentName || "Current list"} disabled={disabled} onChange={(l) => onChange({ ...value, listId: l ? Number(l.id) : null })} />
|
|
59
|
+
</>
|
|
60
|
+
) : (
|
|
61
|
+
<>
|
|
62
|
+
<Label htmlFor={`${idPrefix}-table`} className="mb-1">Table</Label>
|
|
63
|
+
<select id={`${idPrefix}-table`} className={SELECT} value={value.tableId} disabled={disabled || (tables.loading && !tables.data)} onChange={(e) => onChange({ ...value, tableId: e.target.value, tableName: rows.find((t) => t.id === e.target.value)?.name ?? "" })}>
|
|
64
|
+
<option value="">{tables.loading && !tables.data ? "Loading tables" : "Choose a table"}</option>
|
|
65
|
+
{value.tableId && !known ? <option value={value.tableId}>{value.tableName || currentName || "Current table"}</option> : null}
|
|
66
|
+
{rows.map((t) => <option key={t.id} value={t.id}>{t.name ?? "Untitled table"}{t.row_count == null ? "" : ` (${t.row_count.toLocaleString()} rows)`}</option>)}
|
|
67
|
+
</select>
|
|
68
|
+
{tables.error ? <ApiError error={tables.error} title="Could not load tables" onRetry={() => void tables.refetch()} /> : null}
|
|
69
|
+
{tables.data?.more ? <p role="note" className="text-muted-foreground mt-1 text-xs">Showing the first {TABLE_READ} tables.</p> : null}
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|