@graph8/sdk 0.362.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +64 -1
- package/blocks/_foundation/block.json +79 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/block.json +6 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-list.tsx +9 -5
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/block.json +9 -3
- package/blocks/deals/components/deal-detail.tsx +23 -11
- package/blocks/deals/components/deal-related.tsx +1 -1
- package/blocks/deals/components/deals-screen.tsx +10 -5
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/block.json +32 -18
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +5 -2
- package/blocks/forms/components/submissions-table.tsx +3 -1
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/block.json +40 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +10 -6
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/block.json +28 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/quotes/block.json +17 -12
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-related.tsx +5 -2
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2709 -1721
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +22 -19
- package/dist/types/blocks/doctor.d.ts +5 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -15,40 +15,55 @@
|
|
|
15
15
|
"lists"
|
|
16
16
|
],
|
|
17
17
|
"operations": [
|
|
18
|
-
"list_newsletters_newsletters_get",
|
|
19
|
-
"create_newsletter_newsletters_post",
|
|
20
|
-
"list_issues_newsletters__newsletter_id__issues_get",
|
|
21
18
|
"activate_newsletter_newsletters__newsletter_id__activate_post",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
19
|
+
"add_subscriber_newsletters__newsletter_id__subscribers_post",
|
|
20
|
+
"cancel_send_newsletters__newsletter_id__issues__issue_id__send_cancel_post",
|
|
24
21
|
"create_issue_newsletters__newsletter_id__issues_post",
|
|
25
|
-
"
|
|
22
|
+
"create_newsletter_newsletters_post",
|
|
23
|
+
"delete_issue_newsletters__newsletter_id__issues__issue_id__delete",
|
|
24
|
+
"delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
|
|
26
25
|
"generate_newsletter_issue_newsletters__newsletter_id__issues__issue_id__generate_post",
|
|
26
|
+
"get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get",
|
|
27
27
|
"get_issue_generation_status_newsletters__newsletter_id__issues__issue_id__generation_status_get",
|
|
28
|
-
"
|
|
29
|
-
"delete_issue_newsletters__newsletter_id__issues__issue_id__delete",
|
|
30
|
-
"list_subscribers_newsletters__newsletter_id__subscribers_get",
|
|
31
|
-
"send_issue_newsletters__newsletter_id__issues__issue_id__send_post",
|
|
28
|
+
"get_issue_newsletters__newsletter_id__issues__issue_id__get",
|
|
32
29
|
"get_send_status_newsletters__newsletter_id__issues__issue_id__send_get",
|
|
33
|
-
"pause_send_newsletters__newsletter_id__issues__issue_id__send_pause_post",
|
|
34
|
-
"resume_send_newsletters__newsletter_id__issues__issue_id__send_resume_post",
|
|
35
|
-
"cancel_send_newsletters__newsletter_id__issues__issue_id__send_cancel_post",
|
|
36
|
-
"add_subscriber_newsletters__newsletter_id__subscribers_post",
|
|
37
|
-
"list_lists_lists_get",
|
|
38
30
|
"import_subscribers_from_audience_newsletters__newsletter_id__subscribers_import_audience_post",
|
|
31
|
+
"list_issues_newsletters__newsletter_id__issues_get",
|
|
32
|
+
"list_lists_lists_get",
|
|
33
|
+
"list_newsletters_newsletters_get",
|
|
34
|
+
"list_subscribers_newsletters__newsletter_id__subscribers_get",
|
|
35
|
+
"pause_newsletter_newsletters__newsletter_id__pause_post",
|
|
36
|
+
"pause_send_newsletters__newsletter_id__issues__issue_id__send_pause_post",
|
|
39
37
|
"preview_newsletter_subscriber_verification_newsletters__newsletter_id__subscribers_verify_preview_post",
|
|
40
|
-
"
|
|
38
|
+
"resume_send_newsletters__newsletter_id__issues__issue_id__send_resume_post",
|
|
39
|
+
"send_issue_newsletters__newsletter_id__issues__issue_id__send_post",
|
|
40
|
+
"send_test_email_newsletters__newsletter_id__issues__issue_id__send_test_post",
|
|
41
41
|
"unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
42
|
+
"update_issue_newsletters__newsletter_id__issues__issue_id__put",
|
|
43
|
+
"verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post"
|
|
44
44
|
],
|
|
45
45
|
"components": [
|
|
46
46
|
{
|
|
47
47
|
"name": "NewsletterScreen",
|
|
48
48
|
"file": "components/newsletter-screen.tsx",
|
|
49
49
|
"purpose": "Finished newsletter workspace: pick or create a newsletter, issues list with detail (write, send, results), subscribers and performance tabs.",
|
|
50
|
-
"operations": [
|
|
51
|
-
|
|
50
|
+
"operations": [
|
|
51
|
+
"activate_newsletter_newsletters__newsletter_id__activate_post",
|
|
52
|
+
"create_newsletter_newsletters_post",
|
|
53
|
+
"delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
|
|
54
|
+
"get_issue_analytics_kpis_newsletters__newsletter_id__issues__issue_id__analytics_kpis_get",
|
|
55
|
+
"list_issues_newsletters__newsletter_id__issues_get",
|
|
56
|
+
"list_newsletters_newsletters_get",
|
|
57
|
+
"list_subscribers_newsletters__newsletter_id__subscribers_get",
|
|
58
|
+
"pause_newsletter_newsletters__newsletter_id__pause_post",
|
|
59
|
+
"unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch"
|
|
60
|
+
],
|
|
61
|
+
"tiers": [
|
|
62
|
+
"read",
|
|
63
|
+
"write",
|
|
64
|
+
"external",
|
|
65
|
+
"destructive"
|
|
66
|
+
]
|
|
52
67
|
},
|
|
53
68
|
{
|
|
54
69
|
"name": "NewsletterEditor",
|
|
@@ -112,14 +127,15 @@
|
|
|
112
127
|
"file": "components/subscriber-table.tsx",
|
|
113
128
|
"purpose": "Subscribers with search, status filter, add, import a list, verify addresses, unsubscribe and erase.",
|
|
114
129
|
"operations": [
|
|
115
|
-
"list_subscribers_newsletters__newsletter_id__subscribers_get",
|
|
116
130
|
"add_subscriber_newsletters__newsletter_id__subscribers_post",
|
|
117
|
-
"
|
|
131
|
+
"delete_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__delete",
|
|
118
132
|
"import_subscribers_from_audience_newsletters__newsletter_id__subscribers_import_audience_post",
|
|
133
|
+
"list_lists_lists_get",
|
|
134
|
+
"list_newsletters_newsletters_get",
|
|
135
|
+
"list_subscribers_newsletters__newsletter_id__subscribers_get",
|
|
119
136
|
"preview_newsletter_subscriber_verification_newsletters__newsletter_id__subscribers_verify_preview_post",
|
|
120
|
-
"verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post",
|
|
121
137
|
"unsubscribe_subscriber_newsletters__newsletter_id__subscribers__subscriber_id__unsubscribe_patch",
|
|
122
|
-
"
|
|
138
|
+
"verify_newsletter_subscribers_newsletters__newsletter_id__subscribers_verify_post"
|
|
123
139
|
],
|
|
124
140
|
"tiers": [
|
|
125
141
|
"read",
|
|
@@ -25,6 +25,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
|
25
25
|
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
26
26
|
import { generationInProgress, isDraft, issueTitle, sendIsMoving, sendIsPaused, toIsoInstant } from "../lib/newsletter-types";
|
|
27
27
|
import type { Issue, SendState } from "../lib/newsletter-types";
|
|
28
|
+
import { pluralWord } from "@/g8/lib/plural";
|
|
28
29
|
|
|
29
30
|
export interface IssueSchedulerProps {
|
|
30
31
|
newsletterId: string;
|
|
@@ -111,7 +112,7 @@ export function IssueScheduler({ newsletterId, issueId, onSent }: IssueScheduler
|
|
|
111
112
|
{partial ? <p role="alert" className="text-sm text-amber-700">This issue is still being written by AI. Wait for it to finish: sending now would mail the partial version.</p> : null}
|
|
112
113
|
{draft ? (
|
|
113
114
|
<div className="grid gap-3">
|
|
114
|
-
<p className="text-sm">Audience: <strong>{audience.loading ? "..." : subscribers.toLocaleString()}</strong> subscribed {subscribers
|
|
115
|
+
<p className="text-sm">Audience: <strong>{audience.loading ? "..." : subscribers.toLocaleString()}</strong> subscribed {pluralWord(subscribers, "person", "people")}.</p>
|
|
115
116
|
<div className="flex gap-2" role="group" aria-label="When to send">
|
|
116
117
|
<Button size="sm" variant={when === "now" ? "secondary" : "ghost"} onClick={() => setWhen("now")}>Send now</Button>
|
|
117
118
|
<Button size="sm" variant={when === "later" ? "secondary" : "ghost"} onClick={() => setWhen("later")}>Schedule</Button>
|
|
@@ -46,6 +46,7 @@ import { IssueForm } from "./issue-form";
|
|
|
46
46
|
import { IssueScheduler } from "./issue-scheduler";
|
|
47
47
|
import { NewsletterStats } from "./newsletter-stats";
|
|
48
48
|
import { SubscriberTable } from "./subscriber-table";
|
|
49
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
49
50
|
|
|
50
51
|
export interface NewsletterScreenProps {
|
|
51
52
|
/** Open this newsletter first when the URL does not name one. */
|
|
@@ -170,6 +171,7 @@ function SubscriberDetail({ newsletterId, subscriber, onChanged, onClose, expand
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
export function NewsletterScreen({ initialNewsletterId, renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: NewsletterScreenProps) {
|
|
174
|
+
const brand = useBrand();
|
|
173
175
|
const [urlNewsletter, setUrlNewsletter] = useUrlState("newsletter", initialNewsletterId ?? "");
|
|
174
176
|
const [issueParam, setIssueParam] = useUrlState("id", "");
|
|
175
177
|
const [view, setView] = useUrlState("view", "");
|
|
@@ -391,7 +393,7 @@ export function NewsletterScreen({ initialNewsletterId, renderRowActions, render
|
|
|
391
393
|
open={pending !== null}
|
|
392
394
|
tier="external"
|
|
393
395
|
title={pending === "activate" ? `Activate ${current?.name ?? "this newsletter"}?` : `Create the newsletter "${name.trim()}"?`}
|
|
394
|
-
description={<><p>{pending === "activate" ?
|
|
396
|
+
description={<><p>{pending === "activate" ? `Once active, the scheduler sends any issue whose scheduled time has passed to real subscribers. A verified sending domain is required (set it up in ${brand.linkLabel}).` : "This creates the newsletter container. Nothing is sent until you activate it and send an issue."}</p>{(create.error ?? activate.error) ? <ApiError error={create.error ?? activate.error} title="That did not work" /> : null}</>}
|
|
395
397
|
confirmLabel={pending === "activate" ? "Activate" : "Create"}
|
|
396
398
|
confirming={create.loading || activate.loading}
|
|
397
399
|
onCancel={() => setPending(null)}
|
|
@@ -14,27 +14,40 @@
|
|
|
14
14
|
"lists"
|
|
15
15
|
],
|
|
16
16
|
"operations": [
|
|
17
|
-
"
|
|
18
|
-
"pause_nurture_nurtures__nurture_id__pause_post",
|
|
19
|
-
"resume_nurture_nurtures__nurture_id__resume_post",
|
|
20
|
-
"duplicate_nurture_nurtures__nurture_id__duplicate_post",
|
|
21
|
-
"delete_nurture_nurtures__nurture_id__delete",
|
|
22
|
-
"list_my_mailboxes_mailboxes_my_get",
|
|
23
|
-
"list_lists_lists_get",
|
|
17
|
+
"add_contacts_to_list_lists__list_id__contacts_post",
|
|
24
18
|
"create_nurture_nurtures_post",
|
|
25
|
-
"
|
|
19
|
+
"delete_nurture_nurtures__nurture_id__delete",
|
|
20
|
+
"duplicate_nurture_nurtures__nurture_id__duplicate_post",
|
|
26
21
|
"get_nurture_nurtures__nurture_id__get",
|
|
27
|
-
"
|
|
22
|
+
"get_sequence_performance_by_id_analytics_outbound_sequence_performance__sequence_id__get",
|
|
28
23
|
"launch_nurture_nurtures__nurture_id__launch_post",
|
|
29
|
-
"
|
|
24
|
+
"list_lists_lists_get",
|
|
25
|
+
"list_my_mailboxes_mailboxes_my_get",
|
|
26
|
+
"list_nurtures_nurtures_get",
|
|
27
|
+
"pause_nurture_nurtures__nurture_id__pause_post",
|
|
28
|
+
"resume_nurture_nurtures__nurture_id__resume_post",
|
|
29
|
+
"update_nurture_nurtures__nurture_id__patch"
|
|
30
30
|
],
|
|
31
31
|
"components": [
|
|
32
32
|
{
|
|
33
33
|
"name": "NurtureScreen",
|
|
34
34
|
"file": "components/nurture-screen.tsx",
|
|
35
35
|
"purpose": "Finished nurture workspace: list with state and row actions, create sheet, detail with results, audience and launch, and settings.",
|
|
36
|
-
"operations": [
|
|
37
|
-
|
|
36
|
+
"operations": [
|
|
37
|
+
"delete_nurture_nurtures__nurture_id__delete",
|
|
38
|
+
"duplicate_nurture_nurtures__nurture_id__duplicate_post",
|
|
39
|
+
"get_nurture_nurtures__nurture_id__get",
|
|
40
|
+
"list_lists_lists_get",
|
|
41
|
+
"list_my_mailboxes_mailboxes_my_get",
|
|
42
|
+
"list_nurtures_nurtures_get",
|
|
43
|
+
"pause_nurture_nurtures__nurture_id__pause_post",
|
|
44
|
+
"resume_nurture_nurtures__nurture_id__resume_post"
|
|
45
|
+
],
|
|
46
|
+
"tiers": [
|
|
47
|
+
"read",
|
|
48
|
+
"write",
|
|
49
|
+
"destructive"
|
|
50
|
+
]
|
|
38
51
|
},
|
|
39
52
|
{
|
|
40
53
|
"name": "NurtureBuilder",
|
|
@@ -58,9 +71,10 @@
|
|
|
58
71
|
"file": "components/nurture-form.tsx",
|
|
59
72
|
"purpose": "Create a nurture (name, sender mailbox, audience, email steps) or edit its settings.",
|
|
60
73
|
"operations": [
|
|
61
|
-
"list_my_mailboxes_mailboxes_my_get",
|
|
62
|
-
"list_lists_lists_get",
|
|
63
74
|
"create_nurture_nurtures_post",
|
|
75
|
+
"get_nurture_nurtures__nurture_id__get",
|
|
76
|
+
"list_lists_lists_get",
|
|
77
|
+
"list_my_mailboxes_mailboxes_my_get",
|
|
64
78
|
"update_nurture_nurtures__nurture_id__patch"
|
|
65
79
|
],
|
|
66
80
|
"tiers": [
|
|
@@ -25,6 +25,7 @@ import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
|
25
25
|
import { isFinished, isLiveLike, isNurtureOff, isPaused, statusOf } from "../lib/nurture-types";
|
|
26
26
|
import type { Nurture } from "../lib/nurture-types";
|
|
27
27
|
import { NurtureForm } from "./nurture-form";
|
|
28
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
28
29
|
|
|
29
30
|
export interface NurtureBuilderProps {
|
|
30
31
|
/** Called when a nurture is chosen for launch/enrollment (render NurtureEnroll with its id). */
|
|
@@ -35,6 +36,7 @@ export interface NurtureBuilderProps {
|
|
|
35
36
|
type Action = { kind: "pause" | "duplicate"; nurture: Nurture };
|
|
36
37
|
|
|
37
38
|
export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilderProps) {
|
|
39
|
+
const brand = useBrand();
|
|
38
40
|
const [form, setForm] = useState<{ nurture?: Nurture } | null>(null);
|
|
39
41
|
const [toArchive, setToArchive] = useState<Nurture | null>(null);
|
|
40
42
|
const [toResume, setToResume] = useState<Nurture | null>(null);
|
|
@@ -49,7 +51,7 @@ export function NurtureBuilder({ onSelectNurture, pageSize = 50 }: NurtureBuilde
|
|
|
49
51
|
const resume = useG8Mutation((n: Nurture) => g8Post(`/nurtures/${encodeURIComponent(n.id)}/resume`), { onSuccess: () => { setToResume(null); void list.refetch(); } });
|
|
50
52
|
const archive = useG8Mutation((n: Nurture) => g8Delete(`/nurtures/${encodeURIComponent(n.id)}`), { onSuccess: () => { setToArchive(null); void list.refetch(); } });
|
|
51
53
|
|
|
52
|
-
if (isNurtureOff(list.error)) return <EmptyState title="Nurture is not enabled" description=
|
|
54
|
+
if (isNurtureOff(list.error)) return <EmptyState title="Nurture is not enabled" description={`Nurture is behind a feature flag for this workspace. Ask ${brand.linkLabel} to turn it on, then reload.`} />;
|
|
53
55
|
if (list.error) return <ApiError error={list.error} title="Could not load nurtures" onRetry={() => void list.refetch()} />;
|
|
54
56
|
if (form) {
|
|
55
57
|
return (
|
|
@@ -24,6 +24,8 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
24
24
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
25
|
import { isDrafted, isNurtureMissing, isNurtureOff, statusOf } from "../lib/nurture-types";
|
|
26
26
|
import type { ListOption, Nurture } from "../lib/nurture-types";
|
|
27
|
+
import { pluralWord } from "@/g8/lib/plural";
|
|
28
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
27
29
|
|
|
28
30
|
export interface NurtureEnrollProps {
|
|
29
31
|
nurtureId: string;
|
|
@@ -33,6 +35,7 @@ export interface NurtureEnrollProps {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: NurtureEnrollProps) {
|
|
38
|
+
const brand = useBrand();
|
|
36
39
|
const base = `/nurtures/${encodeURIComponent(nurtureId)}`;
|
|
37
40
|
const nurture = useG8Query((signal) => g8Get<Nurture>(base, {}, { signal }).then((r) => r.data), [base]);
|
|
38
41
|
const lists = useG8Query((signal) => g8Get<ListOption[]>("/lists", { limit: 200 }, { signal }).then((r) => r.data ?? []), []);
|
|
@@ -55,7 +58,7 @@ export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: Nurtur
|
|
|
55
58
|
const addContacts = useG8Mutation((id: number) => g8Post(`/lists/${id}/contacts`, { contact_ids: [...contactIds] }), { onSuccess: () => void lists.refetch() });
|
|
56
59
|
const launch = useG8Mutation(() => g8Post(`${base}/launch`), { onSuccess: () => { setConfirming(false); void nurture.refetch(); onLaunched?.(); } });
|
|
57
60
|
|
|
58
|
-
if (isNurtureOff(nurture.error)) return <EmptyState title="Nurture is not enabled" description=
|
|
61
|
+
if (isNurtureOff(nurture.error)) return <EmptyState title="Nurture is not enabled" description={`Nurture is behind a feature flag for this workspace. Ask ${brand.linkLabel} to turn it on.`} />;
|
|
59
62
|
if (isNurtureMissing(nurture.error)) return <EmptyState title="This nurture no longer exists" description="It may have been deleted, or it is not yours to see. Pick another nurture." />;
|
|
60
63
|
if (nurture.error) return <ApiError error={nurture.error} title="Could not load the nurture" onRetry={() => void nurture.refetch()} />;
|
|
61
64
|
if (!nurture.data) return <Skeleton className="h-40 w-full" aria-busy="true" />;
|
|
@@ -76,9 +79,9 @@ export function NurtureEnroll({ nurtureId, contactIds = [], onLaunched }: Nurtur
|
|
|
76
79
|
</select>
|
|
77
80
|
<div className="flex gap-2">
|
|
78
81
|
<Button size="sm" variant="outline" loading={setAudience.loading} disabled={!listId || attached || !isDrafted(nurture.data)} onClick={() => void setAudience.mutate(Number(listId))}>Use this list</Button>
|
|
79
|
-
{contactIds.length > 0 ? <Button size="sm" variant="outline" loading={addContacts.loading} disabled={!attached} onClick={() => void addContacts.mutate(Number(listId))}>Add {contactIds.length} selected {contactIds.length
|
|
82
|
+
{contactIds.length > 0 ? <Button size="sm" variant="outline" loading={addContacts.loading} disabled={!attached} onClick={() => void addContacts.mutate(Number(listId))}>Add {contactIds.length} selected {pluralWord(contactIds.length, "contact")}</Button> : null}
|
|
80
83
|
</div>
|
|
81
|
-
{attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {audience
|
|
84
|
+
{attached ? <p className="text-sm">This nurture will engage <strong>{audience.toLocaleString()}</strong> {pluralWord(audience, "contact")}.</p> : null}
|
|
82
85
|
</div>
|
|
83
86
|
{[setAudience.error, addContacts.error].filter(Boolean).map((e, i) => <ApiError key={i} error={e} title="That did not work" />)}
|
|
84
87
|
{isDrafted(nurture.data) ? (
|
|
@@ -13,13 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { useState } from "react";
|
|
16
|
-
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
16
|
+
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
17
17
|
import { ApiError } from "@/g8/components/api-error";
|
|
18
18
|
import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
19
19
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
20
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
21
|
import { channelPayload, stepsPayload } from "../lib/nurture-types";
|
|
22
22
|
import type { ListOption, MailboxOption, Nurture, StepDraft } from "../lib/nurture-types";
|
|
23
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
23
24
|
|
|
24
25
|
export interface NurtureFormProps {
|
|
25
26
|
/** Edit this nurture; omit to create a new one. */
|
|
@@ -30,7 +31,22 @@ export interface NurtureFormProps {
|
|
|
30
31
|
|
|
31
32
|
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm disabled:opacity-50";
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Edit mode reads the FULL nurture (GET /nurtures/{id}) before it shows anything: the row a list hands over may omit
|
|
36
|
+
* fields (description, audience, flags), and saving a form seeded from it would blank them. Create mode needs no record.
|
|
37
|
+
*/
|
|
33
38
|
export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
|
|
39
|
+
const full = useG8Query((signal) => g8Get<Nurture>(`/nurtures/${encodeURIComponent(nurture!.id)}`, {}, { signal }).then((r) => r.data), [nurture?.id], { enabled: Boolean(nurture) });
|
|
40
|
+
if (nurture) {
|
|
41
|
+
if (full.error && (!full.data || full.data.id !== nurture.id)) return <ApiError error={full.error} title="Could not load this nurture" onRetry={() => void full.refetch()} />;
|
|
42
|
+
if (!full.data || full.data.id !== nurture.id) return <Skeleton className="h-40 w-full" aria-label="Loading nurture" />;
|
|
43
|
+
return <NurtureFormFields key={full.data.id} nurture={full.data} onSaved={onSaved} onCancel={onCancel} />;
|
|
44
|
+
}
|
|
45
|
+
return <NurtureFormFields onSaved={onSaved} onCancel={onCancel} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function NurtureFormFields({ nurture, onSaved, onCancel }: NurtureFormProps) {
|
|
49
|
+
const brand = useBrand();
|
|
34
50
|
const editing = Boolean(nurture);
|
|
35
51
|
const [name, setName] = useState(nurture?.name ?? "");
|
|
36
52
|
const [description, setDescription] = useState(nurture?.description ?? "");
|
|
@@ -50,7 +66,11 @@ export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
|
|
|
50
66
|
const save = useG8Mutation(async () => {
|
|
51
67
|
const flags = { finish_on_reply: finishOnReply, send_in_same_thread: sameThread, include_unsubscribe_cta: unsubscribe };
|
|
52
68
|
if (nurture) {
|
|
53
|
-
|
|
69
|
+
// Only what the person changed is sent.
|
|
70
|
+
const next = { name: name.trim(), description: description.trim() || null, associated_list_id: listId ? Number(listId) : null, ...flags };
|
|
71
|
+
const before = { name: nurture.name, description: nurture.description?.trim() || null, associated_list_id: nurture.associated_list_id ? Number(nurture.associated_list_id) : null, finish_on_reply: nurture.finish_on_reply ?? true, send_in_same_thread: nurture.send_in_same_thread ?? true, include_unsubscribe_cta: nurture.include_unsubscribe_cta ?? true };
|
|
72
|
+
const diff = Object.fromEntries(Object.entries(next).filter(([k, v]) => v !== before[k as keyof typeof before]));
|
|
73
|
+
return (await g8Patch<Nurture>(`/nurtures/${encodeURIComponent(nurture.id)}`, diff)).data;
|
|
54
74
|
}
|
|
55
75
|
return (await g8Post<Nurture>("/nurtures", {
|
|
56
76
|
name: name.trim(),
|
|
@@ -75,7 +95,7 @@ export function NurtureForm({ nurture, onSaved, onCancel }: NurtureFormProps) {
|
|
|
75
95
|
<option value="">{mailboxes.loading ? "Loading..." : "Choose a mailbox"}</option>
|
|
76
96
|
{(mailboxes.data ?? []).map((m) => <option key={m.id} value={m.id}>{m.email}</option>)}
|
|
77
97
|
</select>
|
|
78
|
-
{!editing && !mailboxes.loading && (mailboxes.data ?? []).length === 0 ? <p className="mt-1 text-xs text-amber-700">No mailbox is connected. Connect one in
|
|
98
|
+
{!editing && !mailboxes.loading && (mailboxes.data ?? []).length === 0 ? <p className="mt-1 text-xs text-amber-700">No mailbox is connected. Connect one in {brand.linkLabel} first (Google and Microsoft sign-in happens in the app).</p> : null}
|
|
79
99
|
</div>
|
|
80
100
|
<div>
|
|
81
101
|
<Label htmlFor="g8-nurture-list" className="mb-1">Audience list (consented contacts)</Label>
|
|
@@ -45,6 +45,8 @@ import type { ListOption, MailboxOption, Nurture } from "../lib/nurture-types";
|
|
|
45
45
|
import { NurtureEnroll } from "./nurture-enroll";
|
|
46
46
|
import { NurtureForm } from "./nurture-form";
|
|
47
47
|
import { NurtureStats } from "./nurture-stats";
|
|
48
|
+
import { plural } from "@/g8/lib/plural";
|
|
49
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
48
50
|
|
|
49
51
|
export interface NurtureScreenProps {
|
|
50
52
|
/** Where a person connects a mailbox (Google and Microsoft sign-in only works in the graph8 app). */
|
|
@@ -129,10 +131,12 @@ function NurtureDetails({ nurtureId }: { nurtureId: string }) {
|
|
|
129
131
|
|
|
130
132
|
/** "Wait 1 day" / "Wait 3 days" after the previous email (`time_interval` is seconds). */
|
|
131
133
|
function waitText(days: number): string {
|
|
132
|
-
return `Wait ${days
|
|
134
|
+
return `Wait ${plural(days, "day")} after the previous email`;
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
export function NurtureScreen({ connectMailboxUrl
|
|
137
|
+
export function NurtureScreen({ connectMailboxUrl: connectMailboxUrlProp, contactIds, renderRowActions, renderDetailActions, renderAudienceTools, extraDetailTabs, toolbarActions, className }: NurtureScreenProps) {
|
|
138
|
+
const brand = useBrand();
|
|
139
|
+
const connectMailboxUrl = connectMailboxUrlProp ?? brand.appUrl;
|
|
136
140
|
const [id, setId] = useUrlState("id", "");
|
|
137
141
|
const [view, setView] = useUrlState("view", "");
|
|
138
142
|
const expandProps = { onExpand: () => setView("full"), onCollapse: view === "full" ? () => setView(null) : undefined };
|
|
@@ -158,7 +162,7 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
|
|
|
158
162
|
});
|
|
159
163
|
|
|
160
164
|
if (isNurtureOff(list.error)) {
|
|
161
|
-
return <ScreenLayout title="Nurture" description="Lifecycle email series for people who agreed to hear from you." icon={<HeartHandshake className="size-5" />}><EmptyState title="Nurture is not enabled" description=
|
|
165
|
+
return <ScreenLayout title="Nurture" description="Lifecycle email series for people who agreed to hear from you." icon={<HeartHandshake className="size-5" />}><EmptyState title="Nurture is not enabled" description={`Nurture is switched off for this workspace. Ask ${brand.linkLabel} to turn it on, then reload.`} /></ScreenLayout>;
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
const all = list.data ?? [];
|
|
@@ -177,7 +181,7 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
|
|
|
177
181
|
<li key={n.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${n.id === id ? "bg-muted" : ""}`}>
|
|
178
182
|
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(n.id)}>
|
|
179
183
|
<span className="block truncate font-medium">{n.name}</span>
|
|
180
|
-
<span className="text-muted-foreground block text-xs">{Array.isArray(n.steps) && n.steps.length > 0 ?
|
|
184
|
+
<span className="text-muted-foreground block text-xs">{Array.isArray(n.steps) && n.steps.length > 0 ? plural(n.steps.length, "email") : "Series"}{n.is_shared ? " · shared" : ""}</span>
|
|
181
185
|
</button>
|
|
182
186
|
<StatusPill tone={toneOf(n)}>{statusOf(n)}</StatusPill>
|
|
183
187
|
<div className="flex gap-1">
|
|
@@ -247,8 +251,8 @@ export function NurtureScreen({ connectMailboxUrl = "https://app.graph8.com", co
|
|
|
247
251
|
<FormSheet open={creating} onOpenChange={setCreating} title="New nurture" description="Pick the sender and the audience, then write the emails. Nothing is sent until you go live.">
|
|
248
252
|
<div className="grid gap-4">
|
|
249
253
|
{noMailbox ? (
|
|
250
|
-
<SectionCard title="Connect a mailbox first" description=
|
|
251
|
-
<a className="inline-flex items-center gap-1 text-sm underline" href={connectMailboxUrl} target="_blank" rel="noreferrer">Connect in
|
|
254
|
+
<SectionCard title="Connect a mailbox first" description={`A nurture sends from one connected mailbox. Google and Microsoft sign-in only works inside ${brand.linkLabel}.`}>
|
|
255
|
+
<a className="inline-flex items-center gap-1 text-sm underline" href={connectMailboxUrl} target="_blank" rel="noreferrer">Connect in {brand.linkLabel} <ExternalLink className="size-3.5" /></a>
|
|
252
256
|
</SectionCard>
|
|
253
257
|
) : null}
|
|
254
258
|
{creating ? <NurtureForm onCancel={() => setCreating(false)} onSaved={(created) => { setCreating(false); void list.refetch(); setId(created.id); }} /> : null}
|
package/blocks/objects/README.md
CHANGED
|
@@ -5,6 +5,22 @@ edit and archive their records. `ObjectTable` and `ObjectRecordDetail` are gener
|
|
|
5
5
|
slug, including the standard `contacts`, `companies` and `deals`. This is the HubSpot "custom objects" / Salesforce
|
|
6
6
|
"custom object" screen set.
|
|
7
7
|
|
|
8
|
+
## Screens
|
|
9
|
+
|
|
10
|
+
`ObjectsScreen` (no props) is the finished workspace: a picker of all objects (your own next to the standard contacts, companies and
|
|
11
|
+
deals), the records table with search, load more, create, edit and archive, the open record beside it (values, owners, relationships,
|
|
12
|
+
history, restore) and "Define objects", which opens `ObjectSchemaEditor` in a side sheet so an admin can create objects, add and archive
|
|
13
|
+
fields and rename. The URL carries `?object=` and `?id=`, so a refresh or a shared link lands on the same record. Props:
|
|
14
|
+
`initialObjectSlug`, `allowSchemaEditing` (default true; hide it from non-admins), `toolbarActions`. Needs the same prerequisites as
|
|
15
|
+
the parts (`objects` and `records` in the proxy allow-list, `objects:read`/`write`/`delete`, and `objects:manage` for the editor).
|
|
16
|
+
If the objects API is unreachable the screen says so.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
"use client";
|
|
20
|
+
import { ObjectsScreen } from "@/g8/objects/components/objects-screen";
|
|
21
|
+
export default function Page() { return <ObjectsScreen />; }
|
|
22
|
+
```
|
|
23
|
+
|
|
8
24
|
## Install
|
|
9
25
|
|
|
10
26
|
```
|
|
@@ -82,6 +98,7 @@ export default function Invoices() {
|
|
|
82
98
|
|
|
83
99
|
| Component | File | What it is |
|
|
84
100
|
|---|---|---|
|
|
101
|
+
| `ObjectsScreen` | `components/objects-screen.tsx` | The finished workspace composing everything below. |
|
|
85
102
|
| `ObjectSchemaEditor` | `components/object-schema-editor.tsx` | Create objects, edit their labels, add/edit/archive fields (text, number, select, reference...). |
|
|
86
103
|
| `AttributeFormDialog` | `components/attribute-form-dialog.tsx` | Dialog to add or edit one field of an object. |
|
|
87
104
|
| `ObjectTable` | `components/object-table.tsx` | Records of any object as a table with search over loaded rows, load-more, create, edit and archive. |
|
|
@@ -8,32 +8,33 @@
|
|
|
8
8
|
"generic records table for any object",
|
|
9
9
|
"record detail with owners, relationships and change history",
|
|
10
10
|
"optimistic concurrency on edits",
|
|
11
|
-
"archive and restore with history kept"
|
|
11
|
+
"archive and restore with history kept",
|
|
12
|
+
"finished ObjectsScreen composing the picker, table, record detail and schema editor"
|
|
12
13
|
],
|
|
13
14
|
"dependsOn": [
|
|
14
15
|
"foundation"
|
|
15
16
|
],
|
|
16
17
|
"operations": [
|
|
17
|
-
"list_objects_objects_get",
|
|
18
|
-
"create_object_objects_post",
|
|
19
|
-
"get_object_objects__object_slug__get",
|
|
20
|
-
"list_object_attributes_objects__object_slug__attributes_get",
|
|
21
|
-
"update_object_objects__object_slug__patch",
|
|
22
|
-
"create_attribute_objects__object_slug__attributes_post",
|
|
23
|
-
"update_attribute_objects__object_slug__attributes__attr_slug__patch",
|
|
24
18
|
"archive_attribute_objects__object_slug__attributes__attr_slug__delete",
|
|
25
19
|
"archive_object_objects__object_slug__delete",
|
|
26
|
-
"list_object_records_objects__object_slug__records_get",
|
|
27
|
-
"create_object_record_objects__object_slug__records_post",
|
|
28
|
-
"update_object_record_objects__object_slug__records__record_id__patch",
|
|
29
20
|
"archive_object_record_objects__object_slug__records__record_id__delete",
|
|
30
|
-
"
|
|
21
|
+
"assign_record_role_records__entity_type___entity_id__role_assignments_post",
|
|
22
|
+
"clear_record_role_records__entity_type___entity_id__role_assignments__role_slot__delete",
|
|
23
|
+
"create_attribute_objects__object_slug__attributes_post",
|
|
24
|
+
"create_object_objects_post",
|
|
25
|
+
"create_object_record_objects__object_slug__records_post",
|
|
26
|
+
"get_object_objects__object_slug__get",
|
|
31
27
|
"get_object_record_changes_objects__object_slug__records__record_id__changes_get",
|
|
28
|
+
"get_object_record_objects__object_slug__records__record_id__get",
|
|
32
29
|
"get_object_relationships_objects__object_slug__records__record_id__relationships_get",
|
|
33
|
-
"restore_object_record_objects__object_slug__records__record_id__restore_post",
|
|
34
30
|
"get_record_role_slots_records__entity_type___entity_id__role_slots_get",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
31
|
+
"list_object_attributes_objects__object_slug__attributes_get",
|
|
32
|
+
"list_object_records_objects__object_slug__records_get",
|
|
33
|
+
"list_objects_objects_get",
|
|
34
|
+
"restore_object_record_objects__object_slug__records__record_id__restore_post",
|
|
35
|
+
"update_attribute_objects__object_slug__attributes__attr_slug__patch",
|
|
36
|
+
"update_object_objects__object_slug__patch",
|
|
37
|
+
"update_object_record_objects__object_slug__records__record_id__patch"
|
|
37
38
|
],
|
|
38
39
|
"components": [
|
|
39
40
|
{
|
|
@@ -151,6 +152,17 @@
|
|
|
151
152
|
"write",
|
|
152
153
|
"destructive"
|
|
153
154
|
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "ObjectsScreen",
|
|
158
|
+
"file": "components/objects-screen.tsx",
|
|
159
|
+
"purpose": "Finished custom-objects workspace: object picker, records table, record detail panel and the schema editor in a sheet. No required props.",
|
|
160
|
+
"operations": [
|
|
161
|
+
"list_objects_objects_get"
|
|
162
|
+
],
|
|
163
|
+
"tiers": [
|
|
164
|
+
"read"
|
|
165
|
+
]
|
|
154
166
|
}
|
|
155
167
|
],
|
|
156
168
|
"hooks": [
|
|
@@ -238,6 +250,7 @@
|
|
|
238
250
|
}
|
|
239
251
|
],
|
|
240
252
|
"peerDependencies": {
|
|
241
|
-
"react": ">=18"
|
|
253
|
+
"react": ">=18",
|
|
254
|
+
"lucide-react": ">=0.400.0"
|
|
242
255
|
}
|
|
243
256
|
}
|
|
@@ -15,6 +15,7 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
15
15
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
16
16
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
17
17
|
import { isObjectsApiMissing } from "../lib/object-types";
|
|
18
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
18
19
|
|
|
19
20
|
export interface ObjectNotFoundProps {
|
|
20
21
|
/** What was not found, for the message: "record" or "object". */
|
|
@@ -24,10 +25,11 @@ export interface ObjectNotFoundProps {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function ObjectNotFound({ what, error }: ObjectNotFoundProps) {
|
|
28
|
+
const brand = useBrand();
|
|
27
29
|
const probe = useG8Query((signal) => g8Get<unknown[]>("/objects", {}, { signal }).then(() => "enabled" as const), []);
|
|
28
30
|
if (probe.loading && !probe.data && !probe.error) return <p className="text-muted-foreground text-sm" aria-busy="true">Checking...</p>;
|
|
29
31
|
if (isObjectsApiMissing(probe.error)) {
|
|
30
|
-
return <EmptyState title="Custom objects are unavailable" description=
|
|
32
|
+
return <EmptyState title="Custom objects are unavailable" description={`The ${brand.linkLabel} API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current ${brand.linkLabel} backend, then reload.`} />;
|
|
31
33
|
}
|
|
32
34
|
if (probe.error) return <ApiError error={probe.error} title="Could not check custom objects" onRetry={() => void probe.refetch()} />;
|
|
33
35
|
return <EmptyState title={`This ${what} no longer exists`} description={`It may have been deleted, or the link is out of date. Go back and pick another ${what}.`} />;
|
|
@@ -59,7 +59,8 @@ export function ObjectRecordDetail({ objectSlug, recordId, onArchived, onBack }:
|
|
|
59
59
|
|
|
60
60
|
if (isObjectsApiMissing(record.error)) return <ObjectNotFound what="record" error={record.error} />;
|
|
61
61
|
if (record.error) return <ApiError error={record.error} title="Could not load the record" onRetry={() => void record.refetch()} />;
|
|
62
|
-
|
|
62
|
+
// The hook keeps the previous record while the next loads; edit and archive act on recordId, so show only that record.
|
|
63
|
+
if (!record.data || String(record.data.id) !== String(recordId) || schema.loading) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-64" /><Skeleton className="h-40 w-full" /></div>;
|
|
63
64
|
|
|
64
65
|
const current = record.data;
|
|
65
66
|
const roleEntity = ROLE_ENTITY_TYPES[objectSlug];
|
|
@@ -27,6 +27,7 @@ import { useObjectSchema } from "../hooks/use-object-schema";
|
|
|
27
27
|
import { OBJECT_ICONS, isObjectsApiMissing, isValidSlug, slugify } from "../lib/object-types";
|
|
28
28
|
import type { ObjectAttribute, ObjectDefinition } from "../lib/object-types";
|
|
29
29
|
import { AttributeFormDialog } from "./attribute-form-dialog";
|
|
30
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
30
31
|
|
|
31
32
|
export interface ObjectSchemaEditorProps {
|
|
32
33
|
/** Object to open first. */
|
|
@@ -93,6 +94,7 @@ function ObjectDetailsForm({ object, attributes, onSaved }: { object: ObjectDefi
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: ObjectSchemaEditorProps) {
|
|
97
|
+
const brand = useBrand();
|
|
96
98
|
const [selected, setSelected] = useState<string | undefined>(initialObjectSlug);
|
|
97
99
|
const [editing, setEditing] = useState<ObjectAttribute | "new" | null>(null);
|
|
98
100
|
const [pending, setPending] = useState<Pending | null>(null);
|
|
@@ -113,7 +115,7 @@ export function ObjectSchemaEditor({ initialObjectSlug, onSelectObject }: Object
|
|
|
113
115
|
|
|
114
116
|
if (objects.loading && !objects.data) return <div className="grid gap-2" aria-busy="true"><Skeleton className="h-8 w-48" /><Skeleton className="h-40 w-full" /></div>;
|
|
115
117
|
if (isObjectsApiMissing(objects.error)) {
|
|
116
|
-
return <EmptyState title="Custom objects are unavailable" description=
|
|
118
|
+
return <EmptyState title="Custom objects are unavailable" description={`The ${brand.linkLabel} API at this base URL does not serve custom objects. Check G8_API_URL and that it points at a current ${brand.linkLabel} backend, then reload.`} />;
|
|
117
119
|
}
|
|
118
120
|
if (objects.error) return <ApiError error={objects.error} title="Could not load objects" onRetry={() => void objects.refetch()} />;
|
|
119
121
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { useEffect, useState } from "react";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
16
17
|
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
17
18
|
import { ApiError } from "@/g8/components/api-error";
|
|
18
19
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
@@ -20,6 +21,8 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
20
21
|
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
21
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
23
|
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
24
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
25
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
23
26
|
import { useObjectSchema } from "../hooks/use-object-schema";
|
|
24
27
|
import { formatValue, isObjectsApiMissing, recordTitle } from "../lib/object-types";
|
|
25
28
|
import { ObjectNotFound } from "./object-not-found";
|
|
@@ -27,6 +30,8 @@ import type { ObjectRecord } from "../lib/object-types";
|
|
|
27
30
|
import { RecordFormDialog } from "./record-form-dialog";
|
|
28
31
|
|
|
29
32
|
export interface ObjectTableProps {
|
|
33
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
34
|
+
linkParam?: string;
|
|
30
35
|
objectSlug: string;
|
|
31
36
|
/** Rows per request, 1-200. Default 50. */
|
|
32
37
|
pageSize?: number;
|
|
@@ -36,7 +41,7 @@ export interface ObjectTableProps {
|
|
|
36
41
|
maxColumns?: number;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
|
|
44
|
+
export function ObjectTable({ linkParam, objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
|
|
40
45
|
const schema = useObjectSchema(objectSlug);
|
|
41
46
|
const slug = encodeURIComponent(objectSlug);
|
|
42
47
|
const records = usePaginated<ObjectRecord>(
|
|
@@ -68,6 +73,11 @@ export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColu
|
|
|
68
73
|
return <ApiError error={schema.error ?? records.error} title="Could not load records" onRetry={() => { void schema.refetch(); void records.reload(); }} />;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
const cellText = (c: (typeof columns)[number], i: number, record: ObjectRecord): ReactNode => {
|
|
77
|
+
const text = formatValue(c, record.values?.[c.slug]) || (i === 0 ? recordTitle(schema.definition, schema.attributes, record) : "");
|
|
78
|
+
return i === 0 && onSelectRecord && text ? <RecordLink param={linkParam} id={record.id} onOpen={() => onSelectRecord(record)}>{text}</RecordLink> : text;
|
|
79
|
+
};
|
|
80
|
+
|
|
71
81
|
return (
|
|
72
82
|
<div className="grid gap-3">
|
|
73
83
|
<div className="flex items-center justify-between gap-2">
|
|
@@ -92,9 +102,9 @@ export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColu
|
|
|
92
102
|
</TableHeader>
|
|
93
103
|
<TableBody>
|
|
94
104
|
{rows.map((record) => (
|
|
95
|
-
<TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined}
|
|
105
|
+
<TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined} {...(onSelectRecord ? rowActivationProps(() => onSelectRecord(record)) : {})}>
|
|
96
106
|
{columns.map((c, i) => (
|
|
97
|
-
<TableCell key={c.slug}>{
|
|
107
|
+
<TableCell key={c.slug}>{cellText(c, i, record)}</TableCell>
|
|
98
108
|
))}
|
|
99
109
|
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
|
100
110
|
<Button variant="ghost" size="sm" onClick={() => setForm({ record })}>Edit</Button>
|