@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,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignatureBlockEditor: edit the ordered blocks of a signature template (kind, text, link, image, locked, required), with
|
|
5
|
+
* placeholders to insert, move up and down, remove and add. A pure form control: it calls no operation and owns no
|
|
6
|
+
* state, so SignatureTemplateForm keeps the draft and validates it with `templateProblems`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
|
|
10
|
+
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
11
|
+
import { BLOCK_KINDS, VARIABLES, nextBlockId, variableText } from "../lib/signatures-types";
|
|
12
|
+
import type { BlockKind, TemplateBlock } from "../lib/signatures-types";
|
|
13
|
+
|
|
14
|
+
export interface SignatureBlockEditorProps {
|
|
15
|
+
blocks: TemplateBlock[];
|
|
16
|
+
onChange: (blocks: TemplateBlock[]) => void;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
|
|
21
|
+
|
|
22
|
+
export function SignatureBlockEditor({ blocks, onChange, disabled }: SignatureBlockEditorProps) {
|
|
23
|
+
const patch = (i: number, change: Partial<TemplateBlock>): void => onChange(blocks.map((b, at) => (at === i ? { ...b, ...change } : b)));
|
|
24
|
+
const move = (i: number, by: number): void => {
|
|
25
|
+
const to = i + by;
|
|
26
|
+
if (to < 0 || to >= blocks.length) return;
|
|
27
|
+
const next = [...blocks];
|
|
28
|
+
[next[i], next[to]] = [next[to], next[i]];
|
|
29
|
+
onChange(next);
|
|
30
|
+
};
|
|
31
|
+
const add = (kind: BlockKind): void => onChange([...blocks, { id: nextBlockId(kind, blocks), kind, text: "", url: "", image: "", locked: false, required: false }]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="grid gap-3">
|
|
35
|
+
<ol className="grid gap-3">
|
|
36
|
+
{blocks.map((b, i) => {
|
|
37
|
+
const hint = BLOCK_KINDS.find((k) => k.value === b.kind)?.hint;
|
|
38
|
+
return (
|
|
39
|
+
<li key={i} className="grid gap-2 rounded-md border p-3" aria-label={`Block ${i + 1}: ${b.id}`}>
|
|
40
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
41
|
+
<div className="min-w-40 flex-1">
|
|
42
|
+
<Label htmlFor={`g8-sig-kind-${i}`} className="mb-1">Block {i + 1} type</Label>
|
|
43
|
+
<select id={`g8-sig-kind-${i}`} className={SELECT} value={b.kind} disabled={disabled} onChange={(e) => patch(i, { kind: e.target.value as BlockKind })}>
|
|
44
|
+
{BLOCK_KINDS.map((k) => <option key={k.value} value={k.value}>{k.label}</option>)}
|
|
45
|
+
</select>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="w-40">
|
|
48
|
+
<Label htmlFor={`g8-sig-id-${i}`} className="mb-1">Block id</Label>
|
|
49
|
+
<Input id={`g8-sig-id-${i}`} value={b.id} disabled={disabled} onChange={(e) => patch(i, { id: e.target.value })} />
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex gap-1">
|
|
52
|
+
<Button type="button" size="sm" variant="ghost" aria-label={`Move block ${i + 1} up`} disabled={disabled || i === 0} onClick={() => move(i, -1)}><ArrowUp className="size-4" /></Button>
|
|
53
|
+
<Button type="button" size="sm" variant="ghost" aria-label={`Move block ${i + 1} down`} disabled={disabled || i === blocks.length - 1} onClick={() => move(i, 1)}><ArrowDown className="size-4" /></Button>
|
|
54
|
+
<Button type="button" size="sm" variant="ghost" aria-label={`Remove block ${i + 1}`} disabled={disabled} onClick={() => onChange(blocks.filter((_, at) => at !== i))}><Trash2 className="size-4" /></Button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
{hint ? <p className="text-muted-foreground text-xs">{hint}</p> : null}
|
|
58
|
+
<div>
|
|
59
|
+
<Label htmlFor={`g8-sig-text-${i}`} className="mb-1">Text</Label>
|
|
60
|
+
<Textarea id={`g8-sig-text-${i}`} rows={3} value={b.text} disabled={disabled} onChange={(e) => patch(i, { text: e.target.value })} />
|
|
61
|
+
<select aria-label={`Insert a placeholder into block ${i + 1}`} className={`${SELECT} mt-1`} value="" disabled={disabled} onChange={(e) => e.target.value && patch(i, { text: `${b.text}${variableText(e.target.value)}` })}>
|
|
62
|
+
<option value="">Insert a placeholder</option>
|
|
63
|
+
{VARIABLES.map((v) => <option key={v.token} value={v.token}>{v.label} ({variableText(v.token)})</option>)}
|
|
64
|
+
</select>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
67
|
+
<div><Label htmlFor={`g8-sig-url-${i}`} className="mb-1">Link (optional)</Label><Input id={`g8-sig-url-${i}`} value={b.url} disabled={disabled} placeholder="https://" onChange={(e) => patch(i, { url: e.target.value })} /></div>
|
|
68
|
+
<div><Label htmlFor={`g8-sig-image-${i}`} className="mb-1">Image address (optional)</Label><Input id={`g8-sig-image-${i}`} value={b.image} disabled={disabled} placeholder="https://" onChange={(e) => patch(i, { image: e.target.value })} /></div>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="flex flex-wrap gap-4 text-sm">
|
|
71
|
+
<label className="flex items-center gap-2"><input type="checkbox" checked={b.required} disabled={disabled} onChange={(e) => patch(i, e.target.checked ? { required: true, locked: true } : { required: false })} />Required (always shown)</label>
|
|
72
|
+
<label className="flex items-center gap-2"><input type="checkbox" checked={b.locked} disabled={disabled || b.required} onChange={(e) => patch(i, { locked: e.target.checked })} />Locked (employees cannot change it)</label>
|
|
73
|
+
</div>
|
|
74
|
+
</li>
|
|
75
|
+
);
|
|
76
|
+
})}
|
|
77
|
+
</ol>
|
|
78
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
79
|
+
<select aria-label="Add a block" className={`${SELECT} w-56`} value="" disabled={disabled || blocks.length >= 30} onChange={(e) => e.target.value && add(e.target.value as BlockKind)}>
|
|
80
|
+
<option value="">Add a block</option>
|
|
81
|
+
{BLOCK_KINDS.map((k) => <option key={k.value} value={k.value}>{k.label}</option>)}
|
|
82
|
+
</select>
|
|
83
|
+
<span className="text-muted-foreground flex items-center gap-1 text-xs"><Plus className="size-3" />{blocks.length} of 30 blocks</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignatureCampaignForm: create or edit a signature banner campaign (an optional banner injected into managed signatures
|
|
5
|
+
* for a time window), preview its banner, and test who it would reach.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order (mailboxes and members come from the shared hooks):
|
|
8
|
+
* 1. previewSignatureCampaign (POST /signatures/campaigns/preview) write* renders the banner from the unsaved draft; with a mailbox and
|
|
9
|
+
* recipient addresses it also says whether the campaign would apply and which campaign wins on priority. Changes nothing.
|
|
10
|
+
* 2. createSignatureCampaign (POST /signatures/campaigns) write a campaign; it only applies once its status is Scheduled and the time is inside its dates
|
|
11
|
+
* 3. updateSignatureCampaign (PATCH /signatures/campaigns/{campaign_id}) write REPLACES the campaign and sends the revision that was read (409 if stale)
|
|
12
|
+
* *A POST, so the contract tiers it as a write.
|
|
13
|
+
* The API has no read-one operation: the campaign list row is the complete stored campaign, so an edit is seeded from it and
|
|
14
|
+
* the list is refreshed after every save. Dates are picked in the viewer's time zone and sent as timezone-aware instants,
|
|
15
|
+
* as the API requires. Emptied lists are sent as empty lists. A static native campaign must be sender-wide (no recipient
|
|
16
|
+
* rules), which is checked before the save is offered. Audience-list targeting (recipient_list_ids) has no list picker here, so
|
|
17
|
+
* the block does not depend on the lists block: a campaign that already targets lists keeps them on every save, and the
|
|
18
|
+
* person can remove that targeting; adding lists is done through the API (see the README).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { SafeSignatureFrame } from "./signature-preview";
|
|
25
|
+
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { plural } from "@/g8/lib/plural";
|
|
28
|
+
import { useSignatureMailboxes, useSignatureMembers } from "../hooks/use-signatures-data";
|
|
29
|
+
import { CAMPAIGN_STATUSES, fromLocalInput, isConflict, isManaged, splitList, toLocalInput } from "../lib/signatures-types";
|
|
30
|
+
import type { CampaignData, CampaignPreview, SignatureCampaign } from "../lib/signatures-types";
|
|
31
|
+
|
|
32
|
+
export interface SignatureCampaignFormProps {
|
|
33
|
+
/** Edit this campaign; omit to create one. */
|
|
34
|
+
campaign?: SignatureCampaign;
|
|
35
|
+
onSaved: (campaign: SignatureCampaign) => void;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
onReload?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
41
|
+
|
|
42
|
+
export function SignatureCampaignForm({ campaign, onSaved, onCancel, onReload }: SignatureCampaignFormProps) {
|
|
43
|
+
const d: CampaignData = campaign?.data ?? {};
|
|
44
|
+
const box = useSignatureMailboxes();
|
|
45
|
+
const members = useSignatureMembers();
|
|
46
|
+
const [name, setName] = useState(d.name ?? "");
|
|
47
|
+
const [blockId] = useState(d.block?.id ?? "banner");
|
|
48
|
+
const [text, setText] = useState(d.block?.text ?? "");
|
|
49
|
+
const [url, setUrl] = useState(d.block?.url ?? "");
|
|
50
|
+
const [image, setImage] = useState(d.block?.image ?? "");
|
|
51
|
+
const [starts, setStarts] = useState(toLocalInput(d.starts_at));
|
|
52
|
+
const [ends, setEnds] = useState(toLocalInput(d.ends_at));
|
|
53
|
+
const [status, setStatus] = useState(d.status ?? "draft");
|
|
54
|
+
const [priority, setPriority] = useState(String(d.priority ?? 0));
|
|
55
|
+
const [mailboxIds, setMailboxIds] = useState<number[]>(d.mailbox_ids ?? []);
|
|
56
|
+
const [domains, setDomains] = useState((d.recipient_domains ?? []).join(", "));
|
|
57
|
+
const [excluded, setExcluded] = useState((d.excluded_domains ?? []).join(", "));
|
|
58
|
+
const [external, setExternal] = useState(d.external_only ?? true);
|
|
59
|
+
const [depts, setDepts] = useState<string[]>(d.departments ?? []);
|
|
60
|
+
const [listIds, setListIds] = useState<number[]>(d.recipient_list_ids ?? []);
|
|
61
|
+
const [native, setNative] = useState(d.native_static ?? false);
|
|
62
|
+
const [testMailbox, setTestMailbox] = useState("");
|
|
63
|
+
const [testTo, setTestTo] = useState("");
|
|
64
|
+
|
|
65
|
+
const startIso = fromLocalInput(starts);
|
|
66
|
+
const endIso = fromLocalInput(ends);
|
|
67
|
+
const prio = Number(priority);
|
|
68
|
+
const departments = [...new Set([...(members.data ?? []).map((m) => m.department).filter((x): x is string => Boolean(x)), ...depts])].sort();
|
|
69
|
+
const rules = splitList(domains).length + splitList(excluded).length + listIds.length > 0 || external;
|
|
70
|
+
const problems = [
|
|
71
|
+
name.trim() === "" ? "Give the campaign a name." : "",
|
|
72
|
+
!startIso || !endIso ? "Choose when it starts and ends." : startIso >= endIso ? "It must end after it starts." : "",
|
|
73
|
+
!Number.isInteger(prio) || prio < -1000 || prio > 1000 ? "Priority is a whole number from -1000 to 1000." : "",
|
|
74
|
+
native && rules ? "A static native campaign must go to every recipient: turn off external only and remove the recipient rules." : "",
|
|
75
|
+
].filter(Boolean);
|
|
76
|
+
|
|
77
|
+
const data = (): Record<string, unknown> => ({
|
|
78
|
+
name: name.trim(),
|
|
79
|
+
block: { id: blockId, kind: "banner", text, url, image, locked: false, required: false },
|
|
80
|
+
starts_at: startIso, ends_at: endIso, status, priority: prio,
|
|
81
|
+
mailbox_ids: mailboxIds, recipient_domains: splitList(domains), excluded_domains: splitList(excluded),
|
|
82
|
+
external_only: external, departments: depts, recipient_list_ids: listIds, native_static: native,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const save = useG8Mutation(async (): Promise<SignatureCampaign> => (campaign
|
|
86
|
+
? await g8Patch<SignatureCampaign>(`/signatures/campaigns/${encodeURIComponent(campaign.id)}`, { version: campaign.revision, data: data() })
|
|
87
|
+
: await g8Post<SignatureCampaign>("/signatures/campaigns", { data: data() })).data, {
|
|
88
|
+
onSuccess: onSaved,
|
|
89
|
+
invalidates: [["signatures", "campaigns"]],
|
|
90
|
+
});
|
|
91
|
+
const preview = useG8Mutation(async (): Promise<CampaignPreview> => (await g8Post<CampaignPreview>("/signatures/campaigns/preview", {
|
|
92
|
+
data: data(),
|
|
93
|
+
...(testMailbox ? { mailbox_id: Number(testMailbox), recipients: splitList(testTo), ...(campaign ? { campaign_id: campaign.id } : {}) } : {}),
|
|
94
|
+
})).data);
|
|
95
|
+
const toggle = <T,>(list: T[], v: T, on: boolean): T[] => (on ? [...list, v] : list.filter((x) => x !== v));
|
|
96
|
+
const match = preview.data?.matching as { matches?: boolean; selected_campaign_id?: string | null; limitation?: string } | null | undefined;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<form className="grid gap-4" onSubmit={(e) => { e.preventDefault(); if (problems.length === 0) void save.mutate(undefined); }}>
|
|
100
|
+
<div><Label htmlFor="g8-sig-cname" className="mb-1">Campaign name</Label><Input id="g8-sig-cname" value={name} placeholder="Autumn webinar" onChange={(e) => setName(e.target.value)} /></div>
|
|
101
|
+
<fieldset className="grid gap-2"><legend className="mb-1 text-sm font-medium">Banner</legend>
|
|
102
|
+
<div><Label htmlFor="g8-sig-ctext" className="mb-1">Text (also the image description)</Label><Textarea id="g8-sig-ctext" rows={2} value={text} onChange={(e) => setText(e.target.value)} /></div>
|
|
103
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
104
|
+
<div><Label htmlFor="g8-sig-curl" className="mb-1">Link</Label><Input id="g8-sig-curl" value={url} placeholder="https://" onChange={(e) => setUrl(e.target.value)} /></div>
|
|
105
|
+
<div><Label htmlFor="g8-sig-cimage" className="mb-1">Image address</Label><Input id="g8-sig-cimage" value={image} placeholder="https://" onChange={(e) => setImage(e.target.value)} /></div>
|
|
106
|
+
</div>
|
|
107
|
+
</fieldset>
|
|
108
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
109
|
+
<div><Label htmlFor="g8-sig-cstart" className="mb-1">Starts</Label><Input id="g8-sig-cstart" type="datetime-local" value={starts} onChange={(e) => setStarts(e.target.value)} /></div>
|
|
110
|
+
<div><Label htmlFor="g8-sig-cend" className="mb-1">Ends</Label><Input id="g8-sig-cend" type="datetime-local" value={ends} onChange={(e) => setEnds(e.target.value)} /></div>
|
|
111
|
+
<div>
|
|
112
|
+
<Label htmlFor="g8-sig-cstatus" className="mb-1">Status</Label>
|
|
113
|
+
<select id="g8-sig-cstatus" className={SELECT} value={status} onChange={(e) => setStatus(e.target.value)}>
|
|
114
|
+
{CAMPAIGN_STATUSES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
|
|
115
|
+
{CAMPAIGN_STATUSES.some((s) => s.value === status) ? null : <option value={status}>{status}</option>}
|
|
116
|
+
</select>
|
|
117
|
+
<p className="text-muted-foreground mt-1 text-xs">Only a Scheduled campaign is added to signatures, and only between its dates.</p>
|
|
118
|
+
</div>
|
|
119
|
+
<div><Label htmlFor="g8-sig-cprio" className="mb-1">Priority</Label><Input id="g8-sig-cprio" type="number" value={priority} onChange={(e) => setPriority(e.target.value)} /><p className="text-muted-foreground mt-1 text-xs">When campaigns overlap, the higher number wins.</p></div>
|
|
120
|
+
</div>
|
|
121
|
+
<fieldset className="grid gap-2"><legend className="mb-1 text-sm font-medium">Who sees it</legend>
|
|
122
|
+
<p className="text-muted-foreground text-xs">Leave the mailboxes and departments empty to include everyone.</p>
|
|
123
|
+
{box.mailboxes.length > 0 ? (
|
|
124
|
+
<div className="grid max-h-32 gap-1 overflow-y-auto" role="group" aria-label="Mailboxes">
|
|
125
|
+
{box.mailboxes.map((m) => <label key={m.id} className="flex items-center gap-2 text-sm"><input type="checkbox" checked={mailboxIds.includes(m.id)} onChange={(e) => setMailboxIds(toggle(mailboxIds, m.id, e.target.checked))} />{m.email}</label>)}
|
|
126
|
+
</div>
|
|
127
|
+
) : null}
|
|
128
|
+
{departments.length > 0 ? (
|
|
129
|
+
<div className="flex flex-wrap gap-3" role="group" aria-label="Departments">
|
|
130
|
+
{departments.map((x) => <label key={x} className="flex items-center gap-2 text-sm"><input type="checkbox" checked={depts.includes(x)} onChange={(e) => setDepts(toggle(depts, x, e.target.checked))} />{x}</label>)}
|
|
131
|
+
</div>
|
|
132
|
+
) : null}
|
|
133
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={external} onChange={(e) => setExternal(e.target.checked)} />Only emails to people outside the company</label>
|
|
134
|
+
<div><Label htmlFor="g8-sig-cdomains" className="mb-1">Only these recipient domains</Label><Input id="g8-sig-cdomains" value={domains} placeholder="acme.com, example.org" onChange={(e) => setDomains(e.target.value)} /></div>
|
|
135
|
+
<div><Label htmlFor="g8-sig-cexcl" className="mb-1">Never these recipient domains</Label><Input id="g8-sig-cexcl" value={excluded} onChange={(e) => setExcluded(e.target.value)} /></div>
|
|
136
|
+
{listIds.length > 0 ? (
|
|
137
|
+
<div className="flex flex-wrap items-center gap-2 rounded-md border p-2 text-sm">
|
|
138
|
+
<span>Only people on {plural(listIds.length, "audience list")} (set through the API; kept when you save). They apply only to emails sent from a sequence.</span>
|
|
139
|
+
<Button type="button" size="sm" variant="outline" onClick={() => setListIds([])}>Remove list targeting</Button>
|
|
140
|
+
</div>
|
|
141
|
+
) : null}
|
|
142
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={native} onChange={(e) => setNative(e.target.checked)} />Static native campaign (installed in the mail account, sender-wide)</label>
|
|
143
|
+
</fieldset>
|
|
144
|
+
<section aria-label="Test the campaign" className="grid gap-2 rounded-md border p-3">
|
|
145
|
+
<h3 className="text-sm font-medium">Preview and test</h3>
|
|
146
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
147
|
+
<select aria-label="Test from mailbox" className={SELECT} value={testMailbox} onChange={(e) => setTestMailbox(e.target.value)}><option value="">Only render the banner</option>{box.mailboxes.filter((m) => isManaged(m.deployment)).map((m) => <option key={m.id} value={m.id}>{m.email}</option>)}</select>
|
|
148
|
+
<Input aria-label="Test recipients" value={testTo} placeholder="buyer@acme.com" disabled={testMailbox === ""} onChange={(e) => setTestTo(e.target.value)} />
|
|
149
|
+
</div>
|
|
150
|
+
<div><Button type="button" size="sm" variant="outline" disabled={problems.length > 0} loading={preview.loading} onClick={() => void preview.mutate(undefined)}>Preview</Button></div>
|
|
151
|
+
{preview.error ? <ApiError error={preview.error} title="Could not render the preview" /> : null}
|
|
152
|
+
{preview.data ? <SafeSignatureFrame html={preview.data.html} title="Banner preview" className="border-border h-32 w-full rounded-md border bg-white" /> : null}
|
|
153
|
+
{match ? <p role="status" className="text-sm">{match.matches ? "This campaign would be added to that email." : "This campaign would not be added to that email."} {match.limitation}</p> : null}
|
|
154
|
+
</section>
|
|
155
|
+
{problems.length > 0 ? <ul role="alert" className="text-error list-disc pl-5 text-xs">{problems.map((p) => <li key={p}>{p}</li>)}</ul> : null}
|
|
156
|
+
{save.error && isConflict(save.error) ? (
|
|
157
|
+
<div className="grid gap-2"><ApiError error={save.error} title="Someone else changed this campaign" />{onReload ? <div><Button type="button" size="sm" variant="outline" onClick={onReload}>Load the newer version</Button></div> : null}</div>
|
|
158
|
+
) : save.error ? <ApiError error={save.error} title="Could not save the campaign" /> : null}
|
|
159
|
+
<div className="flex gap-2">
|
|
160
|
+
<Button type="submit" loading={save.loading} disabled={problems.length > 0}>{campaign ? "Save changes" : "Create campaign"}</Button>
|
|
161
|
+
{onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
|
|
162
|
+
</div>
|
|
163
|
+
</form>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignatureCampaigns: the banner campaigns that are added to managed signatures for a time window: the list with status,
|
|
5
|
+
* dates and priority, and a sheet to create or edit one.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations: none directly. The list comes from the shared hook (listSignatureCampaigns, GET /signatures/campaigns,
|
|
8
|
+
* read, not paged: the UI says "All N") and the writes and preview live in SignatureCampaignForm
|
|
9
|
+
* (createSignatureCampaign, updateSignatureCampaign, previewSignatureCampaign). The API has no delete: stop a campaign by
|
|
10
|
+
* setting its status to Paused (or Draft), which keeps its report numbers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Megaphone, Plus } from "lucide-react";
|
|
15
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
19
|
+
import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
20
|
+
import { plural } from "@/g8/lib/plural";
|
|
21
|
+
import { useSignatureCampaigns } from "../hooks/use-signatures-data";
|
|
22
|
+
import { formatWhen } from "../lib/signatures-types";
|
|
23
|
+
import type { SignatureCampaign } from "../lib/signatures-types";
|
|
24
|
+
import { SignatureCampaignForm } from "./signature-campaign-form";
|
|
25
|
+
|
|
26
|
+
export function SignatureCampaigns() {
|
|
27
|
+
const list = useSignatureCampaigns();
|
|
28
|
+
const [editing, setEditing] = useState<SignatureCampaign | "new" | null>(null);
|
|
29
|
+
const rows = list.data ?? [];
|
|
30
|
+
// The edit form is seeded from the campaign as the list holds it now (the API has no read-one), never from a stale copy.
|
|
31
|
+
const current = editing && editing !== "new" ? rows.find((c) => c.id === editing.id) ?? editing : editing;
|
|
32
|
+
|
|
33
|
+
const body = list.error && !list.data ? <ApiError error={list.error} title="Could not load campaigns" onRetry={() => void list.refetch()} /> : !list.data ? (
|
|
34
|
+
<div aria-busy="true" className="grid gap-2"><Skeleton className="h-14 w-full" /><Skeleton className="h-14 w-full" /></div>
|
|
35
|
+
) : rows.length === 0 ? (
|
|
36
|
+
<EmptyState icon={<Megaphone className="size-8" />} title="No campaigns yet" description="A campaign adds a banner to signatures for a time window, for example a webinar or a launch." action={<Button onClick={() => setEditing("new")}>Create a campaign</Button>} />
|
|
37
|
+
) : (
|
|
38
|
+
<>
|
|
39
|
+
<p role="status" className="text-muted-foreground mb-2 text-xs">All {plural(rows.length, "campaign")}</p>
|
|
40
|
+
<ul className="grid gap-2">
|
|
41
|
+
{rows.map((c) => (
|
|
42
|
+
<li key={c.id} className="flex flex-wrap items-center justify-between gap-2 rounded-md border p-3">
|
|
43
|
+
<div className="min-w-0 flex-1">
|
|
44
|
+
<span className="block truncate font-medium">{c.data.name || "Untitled campaign"}</span>
|
|
45
|
+
<span className="text-muted-foreground block text-xs">{formatWhen(c.data.starts_at)} to {formatWhen(c.data.ends_at)} · priority {c.data.priority ?? 0}</span>
|
|
46
|
+
</div>
|
|
47
|
+
<StatusPill tone={toneForStatus(c.data.status)}>{humanizeStatus(c.data.status)}</StatusPill>
|
|
48
|
+
<Button size="sm" variant="outline" aria-label={`Edit ${c.data.name || "campaign"}`} onClick={() => setEditing(c)}>Edit</Button>
|
|
49
|
+
</li>
|
|
50
|
+
))}
|
|
51
|
+
</ul>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="grid gap-3">
|
|
57
|
+
<div className="flex items-center justify-between gap-2">
|
|
58
|
+
<p className="text-muted-foreground text-sm">Banners added to managed signatures between two dates.</p>
|
|
59
|
+
<Button size="sm" onClick={() => setEditing("new")}><Plus className="size-4" />New campaign</Button>
|
|
60
|
+
</div>
|
|
61
|
+
{body}
|
|
62
|
+
<FormSheet open={editing !== null} onOpenChange={(o) => !o && setEditing(null)} width="xl" title={editing === "new" ? "New campaign" : "Edit campaign"} description="A banner shown in signatures during its dates.">
|
|
63
|
+
{current ? (
|
|
64
|
+
<SignatureCampaignForm
|
|
65
|
+
key={current === "new" ? "new" : `${current.id}:${current.revision}`}
|
|
66
|
+
campaign={current === "new" ? undefined : current}
|
|
67
|
+
onSaved={() => setEditing(null)}
|
|
68
|
+
onCancel={() => setEditing(null)}
|
|
69
|
+
onReload={() => void list.refetch()}
|
|
70
|
+
/>
|
|
71
|
+
) : null}
|
|
72
|
+
</FormSheet>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignatureIdentityForm: give one mailbox a managed signature by binding it to an employee (and, optionally, a template).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. previewSignatureSetup (POST /signatures/setup-preview) write* "Check setup": a dry run against the mail provider. Confirms the
|
|
8
|
+
* employee account and the send-as address resolve and shows the signature installed there today. Changes nothing.
|
|
9
|
+
* 2. assignSignatureIdentity (POST /signatures/assignments) write a TAKEOVER: the mailbox's current signature is captured for
|
|
10
|
+
* restore and replaced, and the first install is queued. ConfirmSpendDialog (external) first, naming the mailbox.
|
|
11
|
+
* *A POST, so the contract tiers it as a write.
|
|
12
|
+
* The API requires the employee account to be the employee's own directory email and the send-as address to be the mailbox
|
|
13
|
+
* address, so both are shown read-only and derived from the two choices: nothing is typed or guessed. The mailbox's current
|
|
14
|
+
* revision (0 when it has no assignment yet) is sent so a stale screen cannot overwrite a newer change (409). When the mailbox
|
|
15
|
+
* is already assigned, its template, workspace integration, employee overrides and native-install request are carried over
|
|
16
|
+
* unchanged unless the person edits them here. A provider refusal (422) shows its state and reason.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Label } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useSignatureDeployments, useSignatureMembers, useSignatureTemplates } from "../hooks/use-signatures-data";
|
|
27
|
+
import { isConflict, memberLabel, templateName } from "../lib/signatures-types";
|
|
28
|
+
import type { Assignment, SignatureMailbox } from "../lib/signatures-types";
|
|
29
|
+
|
|
30
|
+
export interface SignatureIdentityFormProps {
|
|
31
|
+
mailbox: SignatureMailbox;
|
|
32
|
+
onDone: () => void;
|
|
33
|
+
onCancel?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface SetupCheck { html?: string; plain?: string; identity_verified?: boolean; write_verified?: boolean; message?: string }
|
|
37
|
+
|
|
38
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
39
|
+
|
|
40
|
+
export function SignatureIdentityForm({ mailbox, onDone, onCancel }: SignatureIdentityFormProps) {
|
|
41
|
+
const members = useSignatureMembers();
|
|
42
|
+
const templates = useSignatureTemplates();
|
|
43
|
+
const deployments = useSignatureDeployments();
|
|
44
|
+
const current = mailbox.deployment?.assignment;
|
|
45
|
+
const [memberId, setMemberId] = useState(current?.member_id ?? "");
|
|
46
|
+
const [templateId, setTemplateId] = useState(current?.template_id ?? "");
|
|
47
|
+
const [native, setNative] = useState(Boolean(current?.native_requested));
|
|
48
|
+
const [confirm, setConfirm] = useState(false);
|
|
49
|
+
const member = members.data?.find((m) => m.id === memberId);
|
|
50
|
+
const usable = Boolean(member?.email);
|
|
51
|
+
|
|
52
|
+
const assignment = (): Assignment => ({
|
|
53
|
+
member_id: memberId,
|
|
54
|
+
...(templateId ? { template_id: templateId } : {}),
|
|
55
|
+
account_subject: member?.email ?? "",
|
|
56
|
+
send_as_email: mailbox.email,
|
|
57
|
+
...(current?.integration_id ? { integration_id: current.integration_id } : {}),
|
|
58
|
+
overrides: current?.overrides ?? {},
|
|
59
|
+
native_requested: native,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const check = useG8Mutation(async (): Promise<SetupCheck> => (await g8Post<SetupCheck>("/signatures/setup-preview", { mailbox_id: mailbox.id, data: assignment() })).data);
|
|
63
|
+
const assign = useG8Mutation(async (): Promise<unknown> => (
|
|
64
|
+
await g8Post("/signatures/assignments", {
|
|
65
|
+
mailbox_ids: [mailbox.id],
|
|
66
|
+
expected_revisions: { [mailbox.id]: mailbox.deployment?.revision ?? 0 },
|
|
67
|
+
confirm_takeover: true,
|
|
68
|
+
data: assignment(),
|
|
69
|
+
})
|
|
70
|
+
).data, {
|
|
71
|
+
onSuccess: () => { setConfirm(false); onDone(); },
|
|
72
|
+
onError: () => setConfirm(false),
|
|
73
|
+
invalidates: [["signatures", "deployments"]],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const err = assign.error;
|
|
77
|
+
return (
|
|
78
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (usable) setConfirm(true); }}>
|
|
79
|
+
<p className="text-muted-foreground text-sm">Mailbox: <span className="text-foreground font-medium">{mailbox.email}</span></p>
|
|
80
|
+
<div>
|
|
81
|
+
<Label htmlFor="g8-sig-employee" className="mb-1">Employee</Label>
|
|
82
|
+
<select id="g8-sig-employee" className={SELECT} value={memberId} onChange={(e) => { setMemberId(e.target.value); check.reset(); }}>
|
|
83
|
+
<option value="">Choose an employee</option>
|
|
84
|
+
{(members.data ?? []).map((m) => <option key={m.id} value={m.id}>{memberLabel(m)}{m.department ? ` (${m.department})` : ""}</option>)}
|
|
85
|
+
</select>
|
|
86
|
+
{memberId !== "" && !usable ? <p role="alert" className="text-error mt-1 text-xs">This employee has no directory email, so a signature cannot be assigned to them.</p> : null}
|
|
87
|
+
</div>
|
|
88
|
+
<div>
|
|
89
|
+
<Label htmlFor="g8-sig-template" className="mb-1">Template</Label>
|
|
90
|
+
<select id="g8-sig-template" className={SELECT} value={templateId} onChange={(e) => setTemplateId(e.target.value)}>
|
|
91
|
+
<option value="">The department or organisation default</option>
|
|
92
|
+
{(templates.data ?? []).map((t) => <option key={t.id} value={t.id}>{templateName(t)}</option>)}
|
|
93
|
+
</select>
|
|
94
|
+
</div>
|
|
95
|
+
<KeyValueList columns={2} items={[
|
|
96
|
+
{ label: "Employee account", value: member?.email ?? "" },
|
|
97
|
+
{ label: "Send-as address", value: mailbox.email },
|
|
98
|
+
]} />
|
|
99
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={native} onChange={(e) => setNative(e.target.checked)} />Also install it inside the mail account when the policy allows native installation</label>
|
|
100
|
+
{check.data ? (
|
|
101
|
+
<div role="status" className="grid gap-1 rounded-md border p-3 text-sm">
|
|
102
|
+
<p>{check.data.message ?? "Checked."}</p>
|
|
103
|
+
{check.data.plain ? <p className="text-muted-foreground text-xs">Signature there today: {check.data.plain}</p> : null}
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
{check.error ? <ApiError error={check.error} title="The mail account could not be checked" /> : null}
|
|
107
|
+
{err && isConflict(err) ? (
|
|
108
|
+
<div className="grid gap-2">
|
|
109
|
+
<ApiError error={err} title="This mailbox changed since you opened it" />
|
|
110
|
+
<div><Button type="button" size="sm" variant="outline" onClick={() => { void deployments.refetch(); onCancel?.(); }}>Reload and start again</Button></div>
|
|
111
|
+
</div>
|
|
112
|
+
) : err ? <ApiError error={err} title="Could not assign the signature" /> : null}
|
|
113
|
+
<div className="flex flex-wrap gap-2">
|
|
114
|
+
<Button type="button" variant="outline" disabled={!usable} loading={check.loading} onClick={() => void check.mutate(undefined)}>Check setup</Button>
|
|
115
|
+
<Button type="submit" disabled={!usable} loading={assign.loading}>{mailbox.deployment ? "Update assignment" : "Assign signature"}</Button>
|
|
116
|
+
{onCancel ? <Button type="button" variant="ghost" onClick={onCancel}>Cancel</Button> : null}
|
|
117
|
+
</div>
|
|
118
|
+
<ConfirmSpendDialog
|
|
119
|
+
open={confirm}
|
|
120
|
+
tier="external"
|
|
121
|
+
title={`Take over the signature of ${mailbox.email}?`}
|
|
122
|
+
affected={1}
|
|
123
|
+
description={<p>The signature this mailbox sends today is saved so it can be restored, then replaced by {templateId ? templateName(templates.data?.find((t) => t.id === templateId)) : "the default template"} for {memberLabel(member)}. New emails from this mailbox carry it.</p>}
|
|
124
|
+
confirmLabel="Take over signature"
|
|
125
|
+
confirming={assign.loading}
|
|
126
|
+
onCancel={() => setConfirm(false)}
|
|
127
|
+
onConfirm={() => void assign.mutate(undefined)}
|
|
128
|
+
/>
|
|
129
|
+
</form>
|
|
130
|
+
);
|
|
131
|
+
}
|