@graph8/sdk 0.370.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/blocks/_foundation/README.md +14 -0
- package/blocks/_foundation/block.json +44 -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/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/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- 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 +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/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 +6 -0
- package/blocks/crm/block.json +53 -4
- 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-lookup-link.tsx +44 -0
- package/blocks/crm/components/contacts-screen.tsx +31 -13
- 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/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/hooks/use-all-lists.ts +18 -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/deals/README.md +24 -6
- package/blocks/deals/block.json +151 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +2 -2
- 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 +4 -1
- 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/enrichment/README.md +16 -6
- package/blocks/enrichment/block.json +19 -7
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +76 -27
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-screen.tsx +70 -17
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +8 -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 +51 -17
- 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 +5 -0
- package/blocks/inbox/block.json +15 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- 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 +36 -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 +113 -1
- 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/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +48 -1
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -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 +6 -0
- package/blocks/sequencer/block.json +28 -1
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +19 -20
- package/blocks/sequencer/components/sequence-detail.tsx +4 -2
- package/blocks/sequencer/components/sequence-enrollments.tsx +4 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequences-screen.tsx +3 -0
- package/blocks/sequencer/components/step-editor.tsx +11 -11
- package/blocks/sequencer/lib/sequencer-utils.ts +5 -5
- 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 +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +1 -1
- package/blocks/workbench/components/workbench-screen.tsx +15 -5
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +536 -392
- 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/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +7 -3
- 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/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +17 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +29 -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 +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +36 -8
- 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 +12 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -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 +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +9 -4
- package/stacks/hubspot/pages/companies.tsx.tmpl +14 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +29 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +10 -1
- 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 +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +21 -5
- 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 +8 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +20 -5
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +27 -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 +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +25 -7
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +15 -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 +4 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +14 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +20 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +10 -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 +7 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +16 -3
- package/styles/styles.css +1 -1
- 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
|
@@ -14,39 +14,64 @@
|
|
|
14
14
|
"lists"
|
|
15
15
|
],
|
|
16
16
|
"operations": [
|
|
17
|
+
"apply_ad_creative_review_findings_ad_creatives__creative_id__ai_review_apply_post",
|
|
18
|
+
"archive_campaign_ad_ad_campaigns__campaign_id__ads__ad_id__delete",
|
|
17
19
|
"cancel_social_post_social_posts__post_id__cancel_post",
|
|
20
|
+
"chat_edit_ad_creative_ad_creatives__creative_id__chat_edit_post",
|
|
18
21
|
"create_ad_creative_ad_creatives_post",
|
|
22
|
+
"create_ad_creative_size_set_ad_creatives__creative_id__size_set_post",
|
|
19
23
|
"create_audience_sync_audience_syncs_post",
|
|
24
|
+
"create_campaign_ad_ad_campaigns__campaign_id__ads_post",
|
|
20
25
|
"create_social_post_social_posts_post",
|
|
21
26
|
"delete_ad_campaign_ad_campaigns__campaign_id__delete",
|
|
22
27
|
"delete_ad_creative_ad_creatives__creative_id__delete",
|
|
23
28
|
"delete_audience_sync_audience_syncs__config_id__delete",
|
|
24
29
|
"delete_social_post_social_posts__post_id__delete",
|
|
30
|
+
"disconnect_social_account_social_accounts__account_id__delete",
|
|
31
|
+
"generate_ad_concepts_ad_creatives_concepts_post",
|
|
25
32
|
"generate_social_post_social_posts_generate_post",
|
|
33
|
+
"get_ad_campaign_ad_campaigns__campaign_id__get",
|
|
34
|
+
"get_ad_creative_ad_creatives__creative_id__get",
|
|
26
35
|
"get_ad_creative_specs_ad_creatives_specs_get",
|
|
27
36
|
"get_ad_creative_status_ad_creatives__creative_id__status_get",
|
|
37
|
+
"get_campaign_editable_fields_ad_campaigns__campaign_id__editable_get",
|
|
28
38
|
"get_per_campaign_metrics_ad_campaigns_metrics_get",
|
|
39
|
+
"get_social_analytics_social_analytics_get",
|
|
40
|
+
"get_social_post_metrics_social_posts__post_id__metrics_get",
|
|
29
41
|
"get_social_post_social_posts__post_id__get",
|
|
30
42
|
"launch_ad_campaign_audience_syncs__config_id__launch_post",
|
|
31
43
|
"list_ad_campaigns_ad_campaigns_get",
|
|
44
|
+
"list_ad_creative_revisions_ad_creatives__creative_id__revisions_get",
|
|
32
45
|
"list_ad_creatives_ad_creatives_get",
|
|
33
46
|
"list_audience_syncs_audience_syncs_get",
|
|
47
|
+
"list_campaign_ads_ad_campaigns__campaign_id__ads_get",
|
|
34
48
|
"list_social_accounts_social_accounts_get",
|
|
49
|
+
"list_social_ideas_social_ideas_get",
|
|
35
50
|
"list_social_posts_social_posts_get",
|
|
36
51
|
"list_sync_errors_audience_syncs__config_id__errors_get",
|
|
37
52
|
"list_sync_runs_audience_syncs__config_id__runs_get",
|
|
53
|
+
"move_ad_creative_to_draft_ad_creatives__creative_id__move_to_draft_post",
|
|
38
54
|
"move_ad_creative_to_gallery_ad_creatives__creative_id__move_to_gallery_post",
|
|
55
|
+
"preview_ad_campaign_edit_ad_campaigns__campaign_id__edit_preview_post",
|
|
39
56
|
"publish_social_post_social_posts__post_id__publish_post",
|
|
40
57
|
"refresh_all_campaign_statuses_ad_campaigns_status_refresh_post",
|
|
41
58
|
"refresh_campaign_status_ad_campaigns__campaign_id__status_refresh_post",
|
|
59
|
+
"regenerate_ad_creative_copy_ad_creatives__creative_id__regenerate_copy_post",
|
|
42
60
|
"regenerate_ad_creative_image_ad_creatives__creative_id__regenerate_image_post",
|
|
61
|
+
"restore_ad_creative_revision_ad_creatives__creative_id__restore_revision_post",
|
|
62
|
+
"retry_ad_creative_generation_ad_creatives__creative_id__retry_post",
|
|
43
63
|
"retry_campaign_launch_ad_campaigns__campaign_id__retry_post",
|
|
64
|
+
"review_ad_creative_ad_creatives__creative_id__ai_review_post",
|
|
65
|
+
"rewrite_ad_creative_field_ad_creatives__creative_id__rewrite_field_post",
|
|
66
|
+
"save_ad_creative_as_template_ad_creatives__creative_id__save_as_template_post",
|
|
44
67
|
"schedule_social_post_social_posts__post_id__schedule_post",
|
|
68
|
+
"set_campaign_ad_status_ad_campaigns__campaign_id__ads__ad_id__status_post",
|
|
45
69
|
"trigger_audience_sync_audience_syncs__config_id__trigger_post",
|
|
70
|
+
"update_ad_campaign_ad_campaigns__campaign_id__put",
|
|
46
71
|
"update_ad_creative_ad_creatives__creative_id__patch",
|
|
47
72
|
"update_audience_sync_audience_syncs__config_id__patch",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
73
|
+
"update_campaign_ad_ad_campaigns__campaign_id__ads__ad_id__patch",
|
|
74
|
+
"update_social_post_social_posts__post_id__patch"
|
|
50
75
|
],
|
|
51
76
|
"components": [
|
|
52
77
|
{
|
|
@@ -146,10 +171,12 @@
|
|
|
146
171
|
"file": "components/social-screen.tsx",
|
|
147
172
|
"purpose": "Finished social page: posts by status with a New post composer sheet, and connected accounts.",
|
|
148
173
|
"operations": [
|
|
149
|
-
"list_social_posts_social_posts_get"
|
|
174
|
+
"list_social_posts_social_posts_get",
|
|
175
|
+
"disconnect_social_account_social_accounts__account_id__delete"
|
|
150
176
|
],
|
|
151
177
|
"tiers": [
|
|
152
|
-
"read"
|
|
178
|
+
"read",
|
|
179
|
+
"destructive"
|
|
153
180
|
]
|
|
154
181
|
},
|
|
155
182
|
{
|
|
@@ -237,6 +264,108 @@
|
|
|
237
264
|
"external",
|
|
238
265
|
"destructive"
|
|
239
266
|
]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"name": "CampaignAds",
|
|
270
|
+
"file": "components/campaign-ads.tsx",
|
|
271
|
+
"purpose": "The ads inside a campaign: list, edit, pause or activate, archive and add (each confirmed).",
|
|
272
|
+
"operations": [
|
|
273
|
+
"update_campaign_ad_ad_campaigns__campaign_id__ads__ad_id__patch",
|
|
274
|
+
"list_campaign_ads_ad_campaigns__campaign_id__ads_get",
|
|
275
|
+
"set_campaign_ad_status_ad_campaigns__campaign_id__ads__ad_id__status_post",
|
|
276
|
+
"archive_campaign_ad_ad_campaigns__campaign_id__ads__ad_id__delete",
|
|
277
|
+
"create_campaign_ad_ad_campaigns__campaign_id__ads_post"
|
|
278
|
+
],
|
|
279
|
+
"tiers": [
|
|
280
|
+
"read",
|
|
281
|
+
"external",
|
|
282
|
+
"destructive"
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"name": "CampaignEditor",
|
|
287
|
+
"file": "components/campaign-editor.tsx",
|
|
288
|
+
"purpose": "Edit a live ad campaign: name, budget, end date, spend cap, pause or resume; previewed, then confirmed.",
|
|
289
|
+
"operations": [
|
|
290
|
+
"preview_ad_campaign_edit_ad_campaigns__campaign_id__edit_preview_post",
|
|
291
|
+
"update_ad_campaign_ad_campaigns__campaign_id__put",
|
|
292
|
+
"get_ad_campaign_ad_campaigns__campaign_id__get",
|
|
293
|
+
"get_campaign_editable_fields_ad_campaigns__campaign_id__editable_get"
|
|
294
|
+
],
|
|
295
|
+
"tiers": [
|
|
296
|
+
"read",
|
|
297
|
+
"external"
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "CreativeConcepts",
|
|
302
|
+
"file": "components/creative-concepts.tsx",
|
|
303
|
+
"purpose": "Concept board: creative directions to react to before generating (confirmed AI spend).",
|
|
304
|
+
"operations": [
|
|
305
|
+
"generate_ad_concepts_ad_creatives_concepts_post"
|
|
306
|
+
],
|
|
307
|
+
"tiers": [
|
|
308
|
+
"billable"
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"name": "CreativeRevisions",
|
|
313
|
+
"file": "components/creative-revisions.tsx",
|
|
314
|
+
"purpose": "Saved versions of an ad creative with restore.",
|
|
315
|
+
"operations": [
|
|
316
|
+
"list_ad_creative_revisions_ad_creatives__creative_id__revisions_get",
|
|
317
|
+
"restore_ad_creative_revision_ad_creatives__creative_id__restore_revision_post"
|
|
318
|
+
],
|
|
319
|
+
"tiers": [
|
|
320
|
+
"read",
|
|
321
|
+
"write"
|
|
322
|
+
]
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"name": "CreativeTools",
|
|
326
|
+
"file": "components/creative-tools.tsx",
|
|
327
|
+
"purpose": "AI tools for a creative: edit by instruction, rewrite a field, review, other sizes, retry, template.",
|
|
328
|
+
"operations": [
|
|
329
|
+
"get_ad_creative_specs_ad_creatives_specs_get",
|
|
330
|
+
"update_ad_creative_ad_creatives__creative_id__patch",
|
|
331
|
+
"apply_ad_creative_review_findings_ad_creatives__creative_id__ai_review_apply_post",
|
|
332
|
+
"move_ad_creative_to_draft_ad_creatives__creative_id__move_to_draft_post",
|
|
333
|
+
"save_ad_creative_as_template_ad_creatives__creative_id__save_as_template_post",
|
|
334
|
+
"chat_edit_ad_creative_ad_creatives__creative_id__chat_edit_post",
|
|
335
|
+
"regenerate_ad_creative_copy_ad_creatives__creative_id__regenerate_copy_post",
|
|
336
|
+
"rewrite_ad_creative_field_ad_creatives__creative_id__rewrite_field_post",
|
|
337
|
+
"review_ad_creative_ad_creatives__creative_id__ai_review_post",
|
|
338
|
+
"create_ad_creative_size_set_ad_creatives__creative_id__size_set_post",
|
|
339
|
+
"retry_ad_creative_generation_ad_creatives__creative_id__retry_post"
|
|
340
|
+
],
|
|
341
|
+
"tiers": [
|
|
342
|
+
"read",
|
|
343
|
+
"write",
|
|
344
|
+
"billable"
|
|
345
|
+
]
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"name": "SocialIdeas",
|
|
349
|
+
"file": "components/social-ideas.tsx",
|
|
350
|
+
"purpose": "Post ideas mined from signals and content, with a Draft a post action.",
|
|
351
|
+
"operations": [
|
|
352
|
+
"list_social_ideas_social_ideas_get"
|
|
353
|
+
],
|
|
354
|
+
"tiers": [
|
|
355
|
+
"read"
|
|
356
|
+
]
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"name": "SocialPerformance",
|
|
360
|
+
"file": "components/social-performance.tsx",
|
|
361
|
+
"purpose": "Social post performance: totals and per-day numbers, and the numbers of one post.",
|
|
362
|
+
"operations": [
|
|
363
|
+
"get_social_analytics_social_analytics_get",
|
|
364
|
+
"get_social_post_metrics_social_posts__post_id__metrics_get"
|
|
365
|
+
],
|
|
366
|
+
"tiers": [
|
|
367
|
+
"read"
|
|
368
|
+
]
|
|
240
369
|
}
|
|
241
370
|
],
|
|
242
371
|
"hooks": [
|
|
@@ -253,6 +382,10 @@
|
|
|
253
382
|
{
|
|
254
383
|
"file": "lib/social-ads-types.ts",
|
|
255
384
|
"purpose": "Post, account, campaign, creative and sync shapes; time helpers (localInputToIso, formatInstant, formatDateOnly), metricsFor and listOf."
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"file": "lib/ads-extra.ts",
|
|
388
|
+
"purpose": "Helpers for the ads and social management components: open-dict readers, editable-field map, campaign edit diff."
|
|
256
389
|
}
|
|
257
390
|
],
|
|
258
391
|
"recipes": [
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { useState } from "react";
|
|
18
18
|
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
19
19
|
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
20
21
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
22
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
23
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -55,10 +56,14 @@ interface SyncError { id: number; error_message?: string; started_at?: string }
|
|
|
55
56
|
|
|
56
57
|
const EMPTY_LAUNCH = { name: "", objective: "", daily_budget: "", currency: "USD", destination_url: "", creative_image_url: "", headline: "", primary_text: "", start_date: "", end_date: "" };
|
|
57
58
|
|
|
59
|
+
/** The runs and errors APIs have no paging: a full read is announced under the list. */
|
|
60
|
+
const RUN_LIMIT = 20;
|
|
61
|
+
const ERROR_LIMIT = 10;
|
|
62
|
+
|
|
58
63
|
function Runs({ syncId }: { syncId: number }) {
|
|
59
64
|
const [confirm, setConfirm] = useState(false);
|
|
60
65
|
const [runs, setRuns] = useState<SyncRun[] | null>(null);
|
|
61
|
-
const load = useG8Mutation(async () => { setRuns(listOf<SyncRun>((await g8Get(`/audience-syncs/${syncId}/runs`, { limit:
|
|
66
|
+
const load = useG8Mutation(async () => { setRuns(listOf<SyncRun>((await g8Get(`/audience-syncs/${syncId}/runs`, { limit: RUN_LIMIT })).data, "runs")); });
|
|
62
67
|
return (
|
|
63
68
|
<SectionCard title="Runs" description="Loading past runs is a billable read, so it asks first.">
|
|
64
69
|
{runs == null ? <Button size="sm" variant="outline" onClick={() => setConfirm(true)}>Load runs</Button> : runs.length === 0 ? <p className="text-sm text-muted-foreground">No runs yet.</p> : (
|
|
@@ -73,6 +78,7 @@ function Runs({ syncId }: { syncId: number }) {
|
|
|
73
78
|
))}
|
|
74
79
|
</ul>
|
|
75
80
|
)}
|
|
81
|
+
{runs ? <FirstNotice shown={runs.length} limit={RUN_LIMIT} noun="runs" /> : null}
|
|
76
82
|
{load.error ? <ApiError error={load.error} onRetry={load.reset} /> : null}
|
|
77
83
|
<ConfirmSpendDialog open={confirm} tier="billable" title="Load sync runs" description="Listing the runs of a sync is classified billable by the API." confirmLabel="Load runs" confirming={load.loading}
|
|
78
84
|
onCancel={() => setConfirm(false)} onConfirm={() => void load.mutate(undefined).then(() => setConfirm(false))} />
|
|
@@ -84,7 +90,7 @@ export function AudienceSyncDetail({ syncId, onClose, onExpand, onCollapse, onCh
|
|
|
84
90
|
const brand = useBrand();
|
|
85
91
|
const { ask, dialog, failure } = useConfirmAction();
|
|
86
92
|
const list = useG8Query(async (signal) => listOf<AudienceSyncConfig>((await g8Get("/audience-syncs", undefined, { signal })).data), []);
|
|
87
|
-
const errors = useG8Query(async (signal) => listOf<SyncError>((await g8Get(`/audience-syncs/${syncId}/errors`, { limit:
|
|
93
|
+
const errors = useG8Query(async (signal) => listOf<SyncError>((await g8Get(`/audience-syncs/${syncId}/errors`, { limit: ERROR_LIMIT }, { signal })).data), [syncId]);
|
|
88
94
|
const [launch, setLaunch] = useState(EMPTY_LAUNCH);
|
|
89
95
|
const [confirmLaunch, setConfirmLaunch] = useState(false);
|
|
90
96
|
const [notice, setNotice] = useState<string | null>(null);
|
|
@@ -154,7 +160,7 @@ export function AudienceSyncDetail({ syncId, onClose, onExpand, onCollapse, onCh
|
|
|
154
160
|
id: "errors",
|
|
155
161
|
label: "Errors",
|
|
156
162
|
content: errors.loading ? <Skeleton className="h-8 w-full" /> : errors.error ? <ApiError error={errors.error} onRetry={() => void errors.refetch()} /> : (errors.data ?? []).length === 0 ? <p className="text-sm text-muted-foreground">No recent errors.</p> : (
|
|
157
|
-
|
|
163
|
+
<><ul className="space-y-1 text-sm">{(errors.data ?? []).map((e) => <li key={e.id}>{e.started_at ? `${formatInstant(e.started_at)}: ` : ""}{e.error_message}</li>)}</ul><FirstNotice shown={(errors.data ?? []).length} limit={ERROR_LIMIT} noun="errors" /></>
|
|
158
164
|
),
|
|
159
165
|
},
|
|
160
166
|
{
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { useState } from "react";
|
|
24
24
|
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
25
25
|
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { FirstNotice } from "@/g8/components/paging-notices";
|
|
26
27
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
28
|
import type { ConfirmTier } from "@/g8/components/confirm-spend-dialog";
|
|
28
29
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
@@ -49,6 +50,9 @@ type Pending =
|
|
|
49
50
|
|
|
50
51
|
const listSize = (l: ListSummary) => l.total ?? l.contact_count ?? l.company_count ?? undefined;
|
|
51
52
|
|
|
53
|
+
/** The errors API has no paging: a full read is announced under the list. */
|
|
54
|
+
const ERROR_LIMIT = 5;
|
|
55
|
+
|
|
52
56
|
export function AudienceSync({ onLaunched, onSelectSync, className }: AudienceSyncProps) {
|
|
53
57
|
const brand = useBrand();
|
|
54
58
|
const [list, setList] = useState<ListSummary | null>(null);
|
|
@@ -63,7 +67,7 @@ export function AudienceSync({ onLaunched, onSelectSync, className }: AudienceSy
|
|
|
63
67
|
const [notice, setNotice] = useState<string | null>(null);
|
|
64
68
|
|
|
65
69
|
const syncs = useG8Query(async (signal) => listOf<AudienceSyncConfig>((await g8Get("/audience-syncs", undefined, { signal })).data), []);
|
|
66
|
-
const errors = useG8Query(async (signal) => listOf<{ id: number; error_message?: string; started_at?: string }>((await g8Get(`/audience-syncs/${errorsFor}/errors`, { limit:
|
|
70
|
+
const errors = useG8Query(async (signal) => listOf<{ id: number; error_message?: string; started_at?: string }>((await g8Get(`/audience-syncs/${errorsFor}/errors`, { limit: ERROR_LIMIT }, { signal })).data), [errorsFor], { enabled: errorsFor != null });
|
|
67
71
|
|
|
68
72
|
const work = async (id: number | "new", fn: () => Promise<unknown>, tolerate404 = false) => {
|
|
69
73
|
setBusy(id);
|
|
@@ -123,7 +127,7 @@ export function AudienceSync({ onLaunched, onSelectSync, className }: AudienceSy
|
|
|
123
127
|
<Button size="sm" variant="ghost" disabled={busy != null} onClick={() => setPending({ kind: "delete", sync: s })}>Delete</Button>
|
|
124
128
|
</div>
|
|
125
129
|
</div>
|
|
126
|
-
{errorsFor === s.id ? <div className="text-xs">{errors.loading ? "Loading..." : (errors.data ?? []).length === 0 ? "No recent errors." :
|
|
130
|
+
{errorsFor === s.id ? <div className="text-xs">{errors.loading ? "Loading..." : (errors.data ?? []).length === 0 ? "No recent errors." : <><ul>{(errors.data ?? []).map((e) => <li key={e.id}>{formatInstant(e.started_at)}: {e.error_message}</li>)}</ul><FirstNotice shown={(errors.data ?? []).length} limit={ERROR_LIMIT} noun="errors" /></>}</div> : null}
|
|
127
131
|
{launch.syncId === s.id ? (
|
|
128
132
|
<form className="grid gap-2 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); if (readyToLaunch) setPending({ kind: "launch", sync: s }); }}>
|
|
129
133
|
{([["name", "Campaign name"], ["objective", "Objective"], ["daily_budget", "Daily budget"], ["destination_url", "Landing page URL"], ["creative_image_url", "Image URL (required on Google)"], ["headline", "Headline"], ["primary_text", "Primary text"]] as const).map(([key, label]) => (
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CampaignAds: the individual ads inside one campaign. See each ad's copy and state, edit an ad, pause or activate it,
|
|
5
|
+
* archive it, and add a new one.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in the order the screen uses them:
|
|
8
|
+
* 1. listCampaignAds GET /ad-campaigns/{id}/ads?include_archived (read)
|
|
9
|
+
* 2. updateCampaignAd PATCH /ad-campaigns/{id}/ads/{ad} (EXTERNAL, confirmed: changes the live ad; only changed fields are sent)
|
|
10
|
+
* 3. setCampaignAdStatus POST /ad-campaigns/{id}/ads/{ad}/status { status } (EXTERNAL, confirmed: starts or stops delivery and spend)
|
|
11
|
+
* 4. archiveCampaignAd DELETE /ad-campaigns/{id}/ads/{ad} (destructive, confirmed: takes the ad down)
|
|
12
|
+
* 5. createCampaignAd POST /ad-campaigns/{id}/ads (EXTERNAL, confirmed: adds an ad to the live campaign)
|
|
13
|
+
*
|
|
14
|
+
* Editing a live ad can make the platform replace it and send it to review again; the dialog says so, and the
|
|
15
|
+
* `consequences` the API returns after an edit are shown. The edit form starts from the loaded ad and sends only what
|
|
16
|
+
* changed.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
25
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
26
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { asRec, num, rowsOf, str } from "../lib/ads-extra";
|
|
30
|
+
import type { Rec } from "../lib/ads-extra";
|
|
31
|
+
|
|
32
|
+
export interface CampaignAdsProps {
|
|
33
|
+
campaignId: number;
|
|
34
|
+
/** The platform the campaign runs on, for the confirm text. */
|
|
35
|
+
platform?: string;
|
|
36
|
+
onChanged?: () => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FIELDS = [
|
|
41
|
+
{ key: "name", label: "Ad name" },
|
|
42
|
+
{ key: "headline", label: "Headline" },
|
|
43
|
+
{ key: "primary_text", label: "Primary text" },
|
|
44
|
+
{ key: "description", label: "Description" },
|
|
45
|
+
{ key: "cta", label: "Call to action" },
|
|
46
|
+
{ key: "destination_url", label: "Destination URL" },
|
|
47
|
+
] as const;
|
|
48
|
+
type Key = (typeof FIELDS)[number]["key"];
|
|
49
|
+
|
|
50
|
+
const tone = (s: string) => (["active", "approved", "running"].includes(s) ? "positive" : ["failed", "rejected", "disapproved"].includes(s) ? "negative" : s === "paused" || s === "archived" ? "neutral" : "info") as "positive" | "negative" | "neutral" | "info";
|
|
51
|
+
|
|
52
|
+
/** The edit body: only fields whose text differs from the saved ad. A URL must be http(s) when changed. */
|
|
53
|
+
export function adChanges(saved: Rec, draft: Record<Key, string>): Partial<Record<Key, string>> {
|
|
54
|
+
const out: Partial<Record<Key, string>> = {};
|
|
55
|
+
for (const { key } of FIELDS) if (draft[key].trim() !== str(saved[key]).trim()) out[key] = draft[key].trim();
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const isHttpUrl = (raw: string): boolean => /^https?:\/\/\S+\.\S+$/i.test(raw.trim());
|
|
60
|
+
|
|
61
|
+
function AdEditor({ ad, base, onClose, onSaved }: { ad: Rec; base: string; onClose: () => void; onSaved: () => void }) {
|
|
62
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
63
|
+
const saved: Record<Key, string> = { name: str(ad.name), headline: str(ad.headline), primary_text: str(ad.primary_text), description: str(ad.description), cta: str(ad.cta), destination_url: str(ad.destination_url) };
|
|
64
|
+
const [edits, setEdits] = useState<Partial<Record<Key, string>>>({});
|
|
65
|
+
const draft = { ...saved, ...edits };
|
|
66
|
+
const changes = adChanges(ad, draft);
|
|
67
|
+
const urlBad = "destination_url" in changes && changes.destination_url !== "" && !isHttpUrl(changes.destination_url ?? "");
|
|
68
|
+
const [note, setNote] = useState("");
|
|
69
|
+
const save = (): void => ask({
|
|
70
|
+
tier: "external", title: `Save changes to "${str(ad.name) || "this ad"}"`, confirmLabel: "Save to the platform",
|
|
71
|
+
description: "This changes the ad on the platform. The platform may replace the ad and review it again, so delivery can pause for a while.",
|
|
72
|
+
run: async () => {
|
|
73
|
+
const d = asRec((await g8Patch(`${base}/ads/${encodeURIComponent(str(ad.id))}`, changes)).data);
|
|
74
|
+
const lines = rowsOf<string>(d.consequences).map(String);
|
|
75
|
+
setNote(`Saved.${d.replaced_platform_ad === true ? " The platform ad was replaced." : ""}${lines.length ? ` ${lines.join(" ")}` : ""}`);
|
|
76
|
+
setEdits({});
|
|
77
|
+
},
|
|
78
|
+
onDone: onSaved,
|
|
79
|
+
});
|
|
80
|
+
return (
|
|
81
|
+
<div className="grid gap-3">
|
|
82
|
+
{FIELDS.map(({ key, label }) => (
|
|
83
|
+
<div key={key}><Label htmlFor={`ca-${key}`} className="mb-1">{label}</Label><Input id={`ca-${key}`} value={draft[key]} aria-invalid={key === "destination_url" && urlBad} onChange={(e) => setEdits({ ...edits, [key]: e.target.value })} /></div>
|
|
84
|
+
))}
|
|
85
|
+
{urlBad ? <p role="alert" className="text-error-foreground text-xs">Enter a full address starting with https://.</p> : null}
|
|
86
|
+
{str(ad.approval_nuance) ? <p className="text-muted-foreground text-xs">{str(ad.approval_nuance)}</p> : null}
|
|
87
|
+
{note ? <p role="status" className="text-sm">{note}</p> : null}
|
|
88
|
+
{failure ? <ApiError error={failure} title="The platform did not accept the change" /> : null}
|
|
89
|
+
<div className="flex justify-end gap-2"><Button variant="outline" onClick={onClose}>Close</Button><Button disabled={Object.keys(changes).length === 0 || urlBad} onClick={save}>Save changes</Button></div>
|
|
90
|
+
<ConfirmSpendDialog {...dialog} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function CampaignAds({ campaignId, platform, onChanged, className }: CampaignAdsProps) {
|
|
96
|
+
const base = `/ad-campaigns/${encodeURIComponent(String(campaignId))}`;
|
|
97
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
98
|
+
const [archived, setArchived] = useState(false);
|
|
99
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
100
|
+
const [adding, setAdding] = useState(false);
|
|
101
|
+
const [fresh, setFresh] = useState<Record<Key, string>>({ name: "", headline: "", primary_text: "", description: "", cta: "", destination_url: "" });
|
|
102
|
+
const list = useG8Query((signal) => g8Get(`${base}/ads`, { include_archived: archived || undefined }, { signal }), [campaignId, archived]);
|
|
103
|
+
const ads = rowsOf<Rec>(asRec(list.data?.data).ads);
|
|
104
|
+
const opened = ads.find((a) => str(a.id) === editing);
|
|
105
|
+
const changed = (): void => { void list.refetch(); onChanged?.(); };
|
|
106
|
+
const where = platform || "the platform";
|
|
107
|
+
const freshBody = Object.fromEntries(Object.entries(fresh).filter(([, v]) => v.trim() !== "").map(([k, v]) => [k, v.trim()]));
|
|
108
|
+
const freshBad = fresh.destination_url.trim() !== "" && !isHttpUrl(fresh.destination_url);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className={className}>
|
|
112
|
+
<SectionCard title="Ads in this campaign" description="Each ad is a version of the copy and image that is shown."
|
|
113
|
+
actions={<div className="flex items-center gap-2"><label className="flex items-center gap-1 text-xs"><input type="checkbox" checked={archived} onChange={(e) => setArchived(e.target.checked)} />Show archived</label><Button size="sm" onClick={() => setAdding(true)}>Add an ad</Button></div>}>
|
|
114
|
+
{list.error ? <ApiError error={list.error} title="Could not load the ads" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? <Skeleton className="h-24 w-full" aria-busy="true" /> : ads.length === 0 ? (
|
|
115
|
+
<EmptyState compact title="No ads yet" description="Add an ad to this campaign, or generate creatives in the Creatives tab." />
|
|
116
|
+
) : (
|
|
117
|
+
<ul className="grid gap-2">
|
|
118
|
+
{ads.map((a) => {
|
|
119
|
+
const status = str(a.status);
|
|
120
|
+
const live = status === "active";
|
|
121
|
+
return (
|
|
122
|
+
<li key={str(a.id)} className="grid gap-2 rounded-md border p-3">
|
|
123
|
+
<div className="flex items-center justify-between gap-2">
|
|
124
|
+
<div className="min-w-0"><span className="block truncate text-sm font-medium">{str(a.headline) || str(a.name) || `Ad ${str(a.slot_index)}`}</span><span className="text-muted-foreground line-clamp-2 text-xs">{str(a.primary_text)}</span></div>
|
|
125
|
+
<StatusPill tone={tone(status)}>{status || "unknown"}</StatusPill>
|
|
126
|
+
</div>
|
|
127
|
+
{str(a.error_message) ? <p role="alert" className="text-error-foreground text-xs">{str(a.error_message)}</p> : null}
|
|
128
|
+
{status !== "archived" ? (
|
|
129
|
+
<div className="flex flex-wrap gap-1">
|
|
130
|
+
<Button size="sm" variant="outline" aria-label={`Edit ${str(a.name) || str(a.headline) || "ad"}`} onClick={() => setEditing(str(a.id))}>Edit</Button>
|
|
131
|
+
{live || status === "paused" ? <Button size="sm" variant="outline" aria-label={`${live ? "Pause" : "Activate"} ${str(a.name) || str(a.headline) || "ad"}`} onClick={() => ask({
|
|
132
|
+
tier: "external", title: `${live ? "Pause" : "Activate"} "${str(a.name) || str(a.headline) || "this ad"}"`, confirmLabel: live ? "Pause ad" : "Activate ad",
|
|
133
|
+
description: live ? `The ad stops being shown on ${where}.` : `The ad starts being shown on ${where} and spends from the campaign budget once approved.`,
|
|
134
|
+
run: () => g8Post(`${base}/ads/${encodeURIComponent(str(a.id))}/status`, { status: live ? "paused" : "active" }), onDone: changed,
|
|
135
|
+
})}>{live ? "Pause" : "Activate"}</Button> : null}
|
|
136
|
+
<Button size="sm" variant="ghost" aria-label={`Archive ${str(a.name) || str(a.headline) || "ad"}`} onClick={() => ask({
|
|
137
|
+
tier: "destructive", title: `Archive "${str(a.name) || str(a.headline) || "this ad"}"`, confirmLabel: "Archive ad", affected: 1,
|
|
138
|
+
description: `The ad is taken down on ${where} and hidden here. It can be shown again with "Show archived".`, tolerateStatus: [404],
|
|
139
|
+
run: () => g8Delete(`${base}/ads/${encodeURIComponent(str(a.id))}`), onDone: changed,
|
|
140
|
+
})}>Archive</Button>
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
143
|
+
</li>
|
|
144
|
+
);
|
|
145
|
+
})}
|
|
146
|
+
</ul>
|
|
147
|
+
)}
|
|
148
|
+
{num(asRec(list.data?.data).count) !== undefined && num(asRec(list.data?.data).count)! > ads.length ? <p role="note" className="text-muted-foreground mt-2 text-xs">Showing {ads.length} of {String(asRec(list.data?.data).count)} ads.</p> : null}
|
|
149
|
+
</SectionCard>
|
|
150
|
+
{failure ? <ApiError error={failure} title="That did not work" /> : null}
|
|
151
|
+
<FormSheet open={Boolean(opened)} onOpenChange={(o) => { if (!o) setEditing(null); }} title="Edit ad" description="Only the fields you change are sent." width="lg">
|
|
152
|
+
{opened ? <AdEditor key={str(opened.id)} ad={opened} base={base} onClose={() => setEditing(null)} onSaved={changed} /> : null}
|
|
153
|
+
</FormSheet>
|
|
154
|
+
<FormSheet open={adding} onOpenChange={setAdding} title="Add an ad" description={`Adds an ad to this live campaign on ${where}.`} width="lg"
|
|
155
|
+
footer={<><Button variant="outline" onClick={() => setAdding(false)}>Cancel</Button><Button disabled={Object.keys(freshBody).length === 0 || freshBad} onClick={() => ask({
|
|
156
|
+
tier: "external", title: "Add this ad to the campaign", confirmLabel: "Add ad",
|
|
157
|
+
description: `The ad is created on ${where} and goes to review. It may start spending once approved.`,
|
|
158
|
+
run: async () => { await g8Post(`${base}/ads`, freshBody); setFresh({ name: "", headline: "", primary_text: "", description: "", cta: "", destination_url: "" }); setAdding(false); }, onDone: changed,
|
|
159
|
+
})}>Add ad</Button></>}>
|
|
160
|
+
{FIELDS.map(({ key, label }) => <div key={key}><Label htmlFor={`cn-${key}`} className="mb-1">{label}</Label><Input id={`cn-${key}`} value={fresh[key]} onChange={(e) => setFresh({ ...fresh, [key]: e.target.value })} /></div>)}
|
|
161
|
+
{freshBad ? <p role="alert" className="text-error-foreground text-xs">Enter a full address starting with https://.</p> : null}
|
|
162
|
+
</FormSheet>
|
|
163
|
+
<ConfirmSpendDialog {...dialog} />
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CampaignDetail: the complete screen for one ad campaign. Finds the campaign by id (so `?id=` deep links work) and shows
|
|
5
5
|
* every field plus its results for a window, and the actions that fit it: refresh its review status, retry a failed launch,
|
|
6
|
-
* stop and delete.
|
|
6
|
+
* stop and delete. The Ads tab lists, edits, pauses, archives and adds the individual ads (CampaignAds); the Edit tab
|
|
7
|
+
* previews and applies changes to the live campaign (CampaignEditor). Date-only values are shown in UTC; spend is in the campaign's own currency.
|
|
7
8
|
*
|
|
8
9
|
* SDK operations, in order:
|
|
9
10
|
* 1. listAdCampaigns GET /ad-campaigns (read)
|
|
@@ -27,6 +28,8 @@ import type { AdCampaign, CampaignMetric } from "../lib/social-ads-types";
|
|
|
27
28
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
28
29
|
import { AskButton, useConfirmAction } from "@/g8/components/confirmed-action";
|
|
29
30
|
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
|
+
import { CampaignAds } from "./campaign-ads";
|
|
32
|
+
import { CampaignEditor } from "./campaign-editor";
|
|
30
33
|
|
|
31
34
|
export interface CampaignDetailProps {
|
|
32
35
|
campaignId: number;
|
|
@@ -121,6 +124,8 @@ export function CampaignDetail({ campaignId, onClose, onExpand, onCollapse, onCh
|
|
|
121
124
|
</SectionCard>
|
|
122
125
|
),
|
|
123
126
|
},
|
|
127
|
+
{ id: "ads", label: "Ads", content: <CampaignAds campaignId={campaign.id} platform={campaign.platform} onChanged={reload} /> },
|
|
128
|
+
{ id: "edit", label: "Edit", content: <CampaignEditor campaignId={campaign.id} onChanged={reload} /> },
|
|
124
129
|
];
|
|
125
130
|
|
|
126
131
|
return (
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CampaignEditor: change a campaign that already exists on an ad platform. Rename it, change the daily budget, the end
|
|
5
|
+
* date or the spend cap, and pause or resume it. Every change is previewed first and pushed to the platform only after
|
|
6
|
+
* a confirmation.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in the order the screen uses them:
|
|
9
|
+
* 1. getAdCampaign GET /ad-campaigns/{id} (read; the saved values the form starts from)
|
|
10
|
+
* 2. getCampaignEditableFields GET /ad-campaigns/{id}/editable (read; which fields this platform and state accept)
|
|
11
|
+
* 3. previewAdCampaignEdit POST /ad-campaigns/{id}/edit-preview (reads the platform, changes nothing; shows the changes and their consequences)
|
|
12
|
+
* 4. updateAdCampaign PUT /ad-campaigns/{id} (EXTERNAL, confirmed: changes the live campaign; only changed fields are sent)
|
|
13
|
+
*
|
|
14
|
+
* The form is read-only until both reads have loaded, so it can never save blanks over a real campaign. A field the
|
|
15
|
+
* platform will not accept is disabled with the platform's own reason. A 200 is not blanket success: a `partial` result
|
|
16
|
+
* lists which changes failed. The spend cap has no saved value to show, so it only changes when you type one.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Pause, Play } from "lucide-react";
|
|
20
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { useConfirmAction } from "@/g8/components/confirmed-action";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
28
|
+
import { asRec, campaignChanges, capabilityOf, dateInput, num, parseMoney, rowsOf, str } from "../lib/ads-extra";
|
|
29
|
+
import type { CampaignEdit, Rec } from "../lib/ads-extra";
|
|
30
|
+
|
|
31
|
+
export interface CampaignEditorProps {
|
|
32
|
+
campaignId: number;
|
|
33
|
+
/** Called after a change reached the platform, so lists and results refresh. */
|
|
34
|
+
onChanged?: () => void;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const strings = (v: unknown): string[] => rowsOf<unknown>(v).map((x) => (typeof x === "string" ? x : str(asRec(x).description) || str(asRec(x).field) || JSON.stringify(x)));
|
|
39
|
+
|
|
40
|
+
function Form({ campaign, editable, onChanged }: { campaign: Rec; editable: Rec; onChanged: () => void }) {
|
|
41
|
+
const base = `/ad-campaigns/${encodeURIComponent(str(campaign.id))}`;
|
|
42
|
+
const { ask, dialog, failure } = useConfirmAction();
|
|
43
|
+
// Only what the person typed is held as `edits`, shown over the saved copy: a reload never wipes typing, and a field they did not touch follows the server.
|
|
44
|
+
const saved = { name: str(campaign.name), daily_budget: num(campaign.daily_budget) == null ? "" : String(campaign.daily_budget), end_date: dateInput(campaign.end_date), spend_cap: "" };
|
|
45
|
+
const [edits, setEdits] = useState<Partial<typeof saved>>({});
|
|
46
|
+
const draft = { ...saved, ...edits };
|
|
47
|
+
const setDraft = (next: typeof saved): void => setEdits(Object.fromEntries(Object.entries(next).filter(([k, v]) => v !== saved[k as keyof typeof saved])) as Partial<typeof saved>);
|
|
48
|
+
const [review, setReview] = useState<{ body: CampaignEdit; changes: string[]; consequences: string[]; warnings: string[] } | null>(null);
|
|
49
|
+
const [outcome, setOutcome] = useState<{ ok: boolean; lines: string[] } | null>(null);
|
|
50
|
+
const body = campaignChanges(campaign, draft);
|
|
51
|
+
const cap = (f: string) => capabilityOf(editable, f);
|
|
52
|
+
const platform = str(campaign.platform);
|
|
53
|
+
const currency = str(campaign.currency, "USD");
|
|
54
|
+
const paused = str(campaign.status) === "paused";
|
|
55
|
+
|
|
56
|
+
const preview = useG8Mutation(async () => {
|
|
57
|
+
const d = asRec((await g8Post(`${base}/edit-preview`, body)).data);
|
|
58
|
+
setReview({ body, changes: strings(d.changes), consequences: strings(d.consequences), warnings: strings(d.warnings) });
|
|
59
|
+
});
|
|
60
|
+
const apply = (b: CampaignEdit | { action: "pause" | "resume" }, title: string, lines: string[]): void => ask({
|
|
61
|
+
tier: "external", title, confirmLabel: "Apply to the platform",
|
|
62
|
+
description: <><p>This changes the live campaign on {platform || "the platform"}. {platform ? `${platform} may review it again and a budget change can restart its learning phase.` : ""}</p>{lines.length ? <ul className="mt-2 list-disc pl-5 text-sm">{lines.map((l, i) => <li key={i}>{l}</li>)}</ul> : null}</>,
|
|
63
|
+
run: async () => {
|
|
64
|
+
const d = asRec((await g8Put(base, b)).data);
|
|
65
|
+
const failed = str(d.status) === "partial";
|
|
66
|
+
setOutcome({ ok: !failed, lines: [...strings(d.changes), ...strings(d.warnings)] });
|
|
67
|
+
setReview(null);
|
|
68
|
+
setEdits({});
|
|
69
|
+
},
|
|
70
|
+
onDone: onChanged,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const field = (key: "name" | "daily_budget" | "end_date" | "spend_cap", label: string, type: string, help?: string) => {
|
|
74
|
+
const c = cap(key);
|
|
75
|
+
return (
|
|
76
|
+
<div>
|
|
77
|
+
<Label htmlFor={`ce-${key}`} className="mb-1">{label}</Label>
|
|
78
|
+
<Input id={`ce-${key}`} type={type} value={draft[key]} disabled={!c.editable} onChange={(e) => { setDraft({ ...draft, [key]: e.target.value }); setReview(null); setOutcome(null); }} />
|
|
79
|
+
{!c.editable ? <p className="text-muted-foreground mt-1 text-xs">{c.reason ?? "The platform does not allow changing this right now."}</p> : help ? <p className="text-muted-foreground mt-1 text-xs">{help}</p> : null}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
const budgetBad = draft.daily_budget.trim() !== "" && cap("daily_budget").editable && parseMoney(draft.daily_budget) === undefined;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="grid gap-4">
|
|
87
|
+
<form className="grid gap-3 sm:grid-cols-2" onSubmit={(e) => { e.preventDefault(); if (Object.keys(body).length) void preview.mutate(undefined); }}>
|
|
88
|
+
{field("name", "Name", "text")}
|
|
89
|
+
{field("daily_budget", `Daily budget (${currency})`, "text", "Spent per day once the platform approves the campaign.")}
|
|
90
|
+
{field("end_date", "End date", "date")}
|
|
91
|
+
{field("spend_cap", `Spend cap (${currency})`, "text", "Pauses the campaign once this much is spent. Leave empty to keep the current cap.")}
|
|
92
|
+
{budgetBad ? <p role="alert" className="text-error-foreground text-xs sm:col-span-2">Enter a positive amount, for example 50 or 49.99.</p> : null}
|
|
93
|
+
<div className="flex gap-2 sm:col-span-2">
|
|
94
|
+
<Button type="submit" variant="outline" loading={preview.loading} disabled={Object.keys(body).length === 0 || budgetBad}>Review changes</Button>
|
|
95
|
+
{cap("status").editable ? (
|
|
96
|
+
<Button type="button" variant="ghost" onClick={() => apply({ action: paused ? "resume" : "pause" }, paused ? "Resume this campaign" : "Pause this campaign", [paused ? "The campaign starts delivering and spending again." : "The campaign stops delivering and spending."])}>
|
|
97
|
+
{paused ? <Play className="size-4" /> : <Pause className="size-4" />}{paused ? "Resume campaign" : "Pause campaign"}
|
|
98
|
+
</Button>
|
|
99
|
+
) : null}
|
|
100
|
+
</div>
|
|
101
|
+
</form>
|
|
102
|
+
{preview.error ? <ApiError error={preview.error} title="Could not preview the changes" /> : null}
|
|
103
|
+
{review ? (
|
|
104
|
+
<SectionCard title="Review before applying" description="Nothing has changed yet.">
|
|
105
|
+
{review.changes.length ? <ul className="list-disc pl-5 text-sm">{review.changes.map((c, i) => <li key={i}>{c}</li>)}</ul> : <p className="text-sm">The platform reports no difference for these values.</p>}
|
|
106
|
+
{review.consequences.map((c, i) => <p key={i} className="text-muted-foreground mt-2 text-xs">{c}</p>)}
|
|
107
|
+
{review.warnings.map((w, i) => <p key={i} role="alert" className="mt-2 text-sm">{w}</p>)}
|
|
108
|
+
<div className="mt-3 flex gap-2"><Button onClick={() => apply(review.body, "Apply these changes", review.changes)}>Apply changes</Button><Button variant="ghost" onClick={() => setReview(null)}>Cancel</Button></div>
|
|
109
|
+
</SectionCard>
|
|
110
|
+
) : null}
|
|
111
|
+
{outcome ? (
|
|
112
|
+
<p role="status" className="text-sm">{outcome.ok ? "Changes applied." : "Some changes were not applied."}{outcome.lines.length ? ` ${outcome.lines.join(" ")}` : ""}</p>
|
|
113
|
+
) : null}
|
|
114
|
+
{failure ? <ApiError error={failure} title="The platform did not accept the change" /> : null}
|
|
115
|
+
<ConfirmSpendDialog {...dialog} />
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function CampaignEditor({ campaignId, onChanged, className }: CampaignEditorProps) {
|
|
121
|
+
const base = `/ad-campaigns/${encodeURIComponent(String(campaignId))}`;
|
|
122
|
+
const campaign = useG8Query((signal) => g8Get(base, undefined, { signal }), [campaignId]);
|
|
123
|
+
const editable = useG8Query((signal) => g8Get(`${base}/editable`, undefined, { signal }), [campaignId]);
|
|
124
|
+
const error = campaign.error ?? editable.error;
|
|
125
|
+
return (
|
|
126
|
+
<div className={className}>
|
|
127
|
+
<SectionCard title="Edit campaign" description="Changes go to the ad platform after you confirm.">
|
|
128
|
+
{error ? <ApiError error={error} title="Could not load the campaign" onRetry={() => { void campaign.refetch(); void editable.refetch(); }} /> : !campaign.data || !editable.data ? <Skeleton className="h-32 w-full" aria-busy="true" /> : (
|
|
129
|
+
<Form key={campaignId} campaign={asRec(campaign.data.data)} editable={asRec(editable.data.data)} onChanged={() => { void campaign.refetch(); void editable.refetch(); onChanged?.(); }} />
|
|
130
|
+
)}
|
|
131
|
+
</SectionCard>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|