@graph8/sdk 0.361.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 +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- 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/empty-state.tsx +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -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/hooks/use-url-state.ts +74 -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/README.md +21 -1
- package/blocks/appointments/block.json +43 -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 +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -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/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- 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/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- 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 +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- 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/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- 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/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- 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 +283 -0
- 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/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- 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 +2785 -1637
- 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 +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- 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 +32 -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/stack.schema.json +6 -1
- 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 +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Account views for the lifecycle record panel: the customer-success brief and recent intent signals.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* AccountBriefTab: GET /customer-success/companies/{id}/brief (lifecycle.getBrief) read, free
|
|
8
|
+
* POST /customer-success/companies/{id}/brief/synthesize (lifecycle.synthesizeBrief) BILLABLE -> ConfirmSpendDialog
|
|
9
|
+
* AccountSignalsTab: GET /accounts/{id}/intent-signals?limit (lifecycle.getAccountIntentSignals) read
|
|
10
|
+
* The company id is the account id on these revenue routes.
|
|
11
|
+
*/
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Radar } from "lucide-react";
|
|
14
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
17
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
18
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
19
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
|
+
import { asRec, body, text } from "../lib/lifecycle";
|
|
23
|
+
|
|
24
|
+
export function AccountBriefTab({ companyId, name }: { companyId: number; name?: string | null }) {
|
|
25
|
+
const base = `/customer-success/companies/${companyId}/brief`;
|
|
26
|
+
const brief = useG8Query((signal) => g8Get(base, undefined, { signal }), [companyId]);
|
|
27
|
+
const [confirm, setConfirm] = useState(false);
|
|
28
|
+
const write = useG8Mutation(() => g8Post(`${base}/synthesize`), { onSuccess: () => { setConfirm(false); void brief.refetch(); } });
|
|
29
|
+
const b = asRec(body(brief.data));
|
|
30
|
+
if (brief.error) return <ApiError error={brief.error} title="Could not load the brief" onRetry={() => void brief.refetch()} />;
|
|
31
|
+
if (brief.loading && !brief.data) return <Skeleton className="h-24 w-full" aria-label="Loading brief" />;
|
|
32
|
+
return (
|
|
33
|
+
<SectionCard title="Account brief" description="What the team knows about this customer, in one place." actions={<Button size="sm" variant="outline" onClick={() => setConfirm(true)}>{b.summary ? "Rewrite brief" : "Write brief"}</Button>}>
|
|
34
|
+
{b.summary ? <p className="whitespace-pre-wrap text-sm" data-testid="brief-summary">{text(b.summary)}</p> : <p className="text-sm text-muted-foreground">No brief written yet.</p>}
|
|
35
|
+
{b.synthesized_at ? <p className="mt-2 text-xs text-muted-foreground">Written <time dateTime={text(b.synthesized_at)}>{new Date(text(b.synthesized_at)).toLocaleDateString()}</time></p> : null}
|
|
36
|
+
{write.error ? <ApiError error={write.error} title="Could not write the brief" /> : null}
|
|
37
|
+
{confirm ? (
|
|
38
|
+
<ConfirmSpendDialog open tier="billable" title={`Write a brief for ${name ?? "this customer"}`} confirmLabel="Write brief" confirming={write.loading}
|
|
39
|
+
description="Reads everything known about the customer and writes a new brief with an AI model. This spends credits." onConfirm={() => void write.mutate(undefined)} onCancel={() => setConfirm(false)} />
|
|
40
|
+
) : null}
|
|
41
|
+
</SectionCard>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function AccountSignalsTab({ companyId }: { companyId: number }) {
|
|
46
|
+
const q = useG8Query((signal) => g8Get(`/accounts/${companyId}/intent-signals`, { limit: 20 }, { signal }), [companyId]);
|
|
47
|
+
if (q.error) return <ApiError error={q.error} title="Could not load signals" onRetry={() => void q.refetch()} />;
|
|
48
|
+
if (q.loading && !q.data) return <Skeleton className="h-24 w-full" aria-label="Loading signals" />;
|
|
49
|
+
const payload = body(q.data);
|
|
50
|
+
const raw = Array.isArray(payload) ? payload : Array.isArray(asRec(payload).signals) ? (asRec(payload).signals as unknown[]) : [];
|
|
51
|
+
const signals = raw.map(asRec);
|
|
52
|
+
if (signals.length === 0) return <EmptyState icon={<Radar className="size-8" />} title="Quiet" description="No recent buying or activity signals for this account." />;
|
|
53
|
+
return (
|
|
54
|
+
<ul className="divide-y rounded-md border" aria-label="Signals">
|
|
55
|
+
{signals.map((s, i) => {
|
|
56
|
+
const when = text(s.detected_at ?? s.created_at ?? s.occurred_at);
|
|
57
|
+
return (
|
|
58
|
+
<li key={text(s.id, String(i))} className="flex items-center justify-between gap-3 p-3 text-sm">
|
|
59
|
+
<span className="min-w-0 truncate">{text(s.title ?? s.signal ?? s.signal_type ?? s.type ?? s.name, "Signal")}</span>
|
|
60
|
+
{when ? <time className="shrink-0 text-xs text-muted-foreground" dateTime={when} title={new Date(when).toLocaleString()}>{new Date(when).toLocaleDateString()}</time> : null}
|
|
61
|
+
</li>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</ul>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -25,6 +25,7 @@ import { useLifecycleStages } from "../hooks/use-lifecycle-stages";
|
|
|
25
25
|
import { asRec, body, money } from "../lib/lifecycle";
|
|
26
26
|
import type { CompanyRecord, LifecycleStage } from "../lib/lifecycle";
|
|
27
27
|
import { StageMoveDialog } from "./stage-move-dialog";
|
|
28
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
28
29
|
|
|
29
30
|
export interface LifecycleBoardProps {
|
|
30
31
|
/** Open a company in your detail route (CompanyDetail from the crm block, for example). */
|
|
@@ -77,13 +78,14 @@ function Column({ stage, count, pageSize, version, onOpenCompany, onMove }: Colu
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
export function LifecycleBoard({ onOpenCompany, pageSize = 25, className }: LifecycleBoardProps) {
|
|
81
|
+
const brand = useBrand();
|
|
80
82
|
const { stages, countOf, loading, error, refetch } = useLifecycleStages();
|
|
81
83
|
const [moving, setMoving] = useState<{ record: CompanyRecord; stage: LifecycleStage } | null>(null);
|
|
82
84
|
const [version, setVersion] = useState(0);
|
|
83
85
|
|
|
84
86
|
if (loading) return <div className={className} aria-busy="true"><Skeleton className="h-48 w-full" /></div>;
|
|
85
87
|
if (error) return <ApiError error={error} onRetry={() => void refetch()} />;
|
|
86
|
-
if (stages.length === 0) return <EmptyState title="No lifecycle stages" description=
|
|
88
|
+
if (stages.length === 0) return <EmptyState title="No lifecycle stages" description={`Stages are configured in ${brand.linkLabel} under Revenue settings.`} />;
|
|
87
89
|
|
|
88
90
|
return (
|
|
89
91
|
<div className={className}>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LifecycleScreen: the finished revenue-lifecycle product. One screen for Suspects, Leads, Trial, Customers and
|
|
5
|
+
* Churned (and any custom stage): the board, each stage's working list, and a record panel. No props are required.
|
|
6
|
+
*
|
|
7
|
+
* It composes: LifecycleBoard (columns with counts and Move), LeadQueue (contacts of a stage: search, billable
|
|
8
|
+
* unlock, move), CustomerTable (MRR, headline counts, AI account brief), ChurnList (last MRR, activity check,
|
|
9
|
+
* win-back), StageMoveDialog, and the crm block's CompanyDetail and ContactDetail for the open record.
|
|
10
|
+
*
|
|
11
|
+
* Flows:
|
|
12
|
+
* - Headline numbers: how many companies sit in every stage.
|
|
13
|
+
* - Board tab: move a company between stages (a dry run shows refusals first, for example Customers without an
|
|
14
|
+
* active subscription). One tab per stage after it: Customers shows the book of business with an account brief
|
|
15
|
+
* (rewriting it uses credits, confirmed); Churned shows lost revenue and a win-back to Leads; every other stage
|
|
16
|
+
* is a contact queue where locked emails can be unlocked (credits, confirmed).
|
|
17
|
+
* - Open a company or contact (deep link ?rec=company:<id> or ?rec=contact:<id>, full page with &view=full) -> the
|
|
18
|
+
* full CRM record (activity, notes, tasks, edit) with a Move stage action; a company also gets Brief (rewrite
|
|
19
|
+
* uses credits, confirmed) and Signals tabs. Leads are the contacts of a stage, customers and churned are companies.
|
|
20
|
+
*
|
|
21
|
+
* SDK operations (through the composed parts): listLifecycleStages, getLifecycleStageCounts,
|
|
22
|
+
* listLifecycleStageRecords, moveCompanyLifecycleStage (dry run first), moveContactLifecycleStage,
|
|
23
|
+
* listLifecycleContacts, unlockLifecycleContacts (billable), getRevenueSummary, getBrief, synthesizeBrief
|
|
24
|
+
* (billable), getAccountIntentSignals, plus the crm block's getCompany / getContact.
|
|
25
|
+
*
|
|
26
|
+
* Slots (all optional): renderDetailActions, extraDetailTabs, toolbarActions.
|
|
27
|
+
*/
|
|
28
|
+
import { useState, type ReactNode } from "react";
|
|
29
|
+
import { GitBranch } from "lucide-react";
|
|
30
|
+
import { Button } from "@graph8/sdk/ui";
|
|
31
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
32
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
33
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
34
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
35
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
36
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
37
|
+
import { CompanyDetail } from "@/g8/crm/components/company-detail";
|
|
38
|
+
import { ContactDetail } from "@/g8/crm/components/contact-detail";
|
|
39
|
+
import type { CompanyRecord as CrmCompany, ContactRecord as CrmContact } from "@/g8/crm/lib/crm-types";
|
|
40
|
+
import { useLifecycleStages } from "../hooks/use-lifecycle-stages";
|
|
41
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
42
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
43
|
+
import type { LifecycleStage } from "../lib/lifecycle";
|
|
44
|
+
import { AccountBriefTab, AccountSignalsTab } from "./account-related";
|
|
45
|
+
import { ChurnList } from "./churn-list";
|
|
46
|
+
import { CustomerTable } from "./customer-table";
|
|
47
|
+
import { LeadQueue } from "./lead-queue";
|
|
48
|
+
import { LifecycleBoard } from "./lifecycle-board";
|
|
49
|
+
import { StageMoveDialog } from "./stage-move-dialog";
|
|
50
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
51
|
+
|
|
52
|
+
export interface LifecycleScreenProps {
|
|
53
|
+
/** Buttons next to "Move stage" in the record panel. */
|
|
54
|
+
renderDetailActions?: (record: { kind: "company" | "contact"; id: number }) => ReactNode;
|
|
55
|
+
/** Extra tabs in the record panel (for example Deals or Quotes for a company). */
|
|
56
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (record: { kind: "company" | "contact"; id: number }) => ReactNode }>;
|
|
57
|
+
toolbarActions?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type OpenRecord = { kind: "company" | "contact"; id: number };
|
|
61
|
+
|
|
62
|
+
function parseRecord(raw: string | null | undefined): OpenRecord | null {
|
|
63
|
+
const [kind, id] = (raw ?? "").split(":");
|
|
64
|
+
const n = Number(id);
|
|
65
|
+
return (kind === "company" || kind === "contact") && Number.isInteger(n) && n > 0 ? { kind, id: n } : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The brief tab, named after the company so the billable confirm says which customer it writes for. */
|
|
69
|
+
function CompanyBrief({ companyId }: { companyId: number }) {
|
|
70
|
+
const company = useG8Query((signal) => g8Get<CrmCompany>(`/companies/${companyId}`, undefined, { signal }), [companyId]);
|
|
71
|
+
const name = company.data?.data?.name ?? null;
|
|
72
|
+
return <AccountBriefTab companyId={companyId} name={name} />;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function StageTab({ stage, version, onOpenCompany, onOpenContact }: { stage: LifecycleStage; version: number; onOpenCompany: (id: number) => void; onOpenContact: (id: number) => void }) {
|
|
76
|
+
if (stage.slug === "customers") return <CustomerTable key={version} stageSlug={stage.slug} onOpenCompany={onOpenCompany} />;
|
|
77
|
+
if (stage.slug === "churned") return <ChurnList key={version} stageSlug={stage.slug} onOpenCompany={onOpenCompany} />;
|
|
78
|
+
return <LeadQueue key={`${stage.slug}-${version}`} stageSlug={stage.slug} onOpenContact={onOpenContact} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function LifecycleScreen({ renderDetailActions, extraDetailTabs, toolbarActions }: LifecycleScreenProps) {
|
|
82
|
+
const brand = useBrand();
|
|
83
|
+
const { stages, countOf, loading, error, refetch } = useLifecycleStages();
|
|
84
|
+
const [tabParam, setTab] = useUrlState("tab", "board");
|
|
85
|
+
const [raw, setRaw] = useUrlState("rec");
|
|
86
|
+
const [mode, setMode] = useUrlState("view");
|
|
87
|
+
const [version, setVersion] = useState(0);
|
|
88
|
+
const [moving, setMoving] = useState<{ record: OpenRecord; name: string } | null>(null);
|
|
89
|
+
const record = parseRecord(raw);
|
|
90
|
+
const tab = tabParam === "board" || stages.some((s) => s.slug === tabParam) ? (tabParam as string) : "board";
|
|
91
|
+
const activeStage = stages.find((s) => s.slug === tab);
|
|
92
|
+
const close = () => { setRaw(null); setMode(null); };
|
|
93
|
+
const openCompany = (id: number) => setRaw(`company:${id}`);
|
|
94
|
+
const openContact = (id: number) => setRaw(`contact:${id}`);
|
|
95
|
+
const refresh = () => { setVersion((v) => v + 1); void refetch(); };
|
|
96
|
+
|
|
97
|
+
const moveButton = (name: string | null | undefined) => record ? (
|
|
98
|
+
<>
|
|
99
|
+
<Button variant="outline" size="sm" onClick={() => setMoving({ record, name: name || `${record.kind} ${record.id}` })}>Move stage</Button>
|
|
100
|
+
{renderDetailActions?.(record)}
|
|
101
|
+
</>
|
|
102
|
+
) : null;
|
|
103
|
+
|
|
104
|
+
const detail = record ? (
|
|
105
|
+
<DetailPanel
|
|
106
|
+
title={record.kind === "company" ? "Company" : "Contact"}
|
|
107
|
+
onClose={close}
|
|
108
|
+
onExpand={mode === "full" ? undefined : () => setMode("full")}
|
|
109
|
+
onCollapse={mode === "full" ? () => setMode(null) : undefined}
|
|
110
|
+
tabs={[
|
|
111
|
+
{
|
|
112
|
+
id: "record",
|
|
113
|
+
label: "Record",
|
|
114
|
+
content: record.kind === "company"
|
|
115
|
+
? <CompanyDetail key={record.id} companyId={record.id} onOpenContact={(c: CrmContact) => openContact(c.id)} renderActions={(c: CrmCompany) => moveButton(c.name)} extraTabs={(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, render: () => t.render(record) }))} />
|
|
116
|
+
: <ContactDetail key={record.id} contactId={record.id} onOpenCompany={openCompany} renderActions={(c: CrmContact) => moveButton(c.full_name || [c.first_name, c.last_name].filter(Boolean).join(" "))} extraTabs={(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, render: () => t.render(record) }))} />,
|
|
117
|
+
},
|
|
118
|
+
...(record.kind === "company" ? [
|
|
119
|
+
{ id: "brief", label: "Brief", content: <CompanyBrief key={record.id} companyId={record.id} /> },
|
|
120
|
+
{ id: "signals", label: "Signals", content: <AccountSignalsTab key={record.id} companyId={record.id} /> },
|
|
121
|
+
] : []),
|
|
122
|
+
]}
|
|
123
|
+
/>
|
|
124
|
+
) : null;
|
|
125
|
+
|
|
126
|
+
let body: ReactNode;
|
|
127
|
+
if (error) body = <ApiError error={error} title="Could not load lifecycle stages" onRetry={() => void refetch()} />;
|
|
128
|
+
else if (!loading && stages.length === 0) body = <EmptyState title="No lifecycle stages" description={`Stages are configured in ${brand.linkLabel} under Revenue settings.`} />;
|
|
129
|
+
else if (activeStage) body = <StageTab stage={activeStage} version={version} onOpenCompany={openCompany} onOpenContact={openContact} />;
|
|
130
|
+
else body = <LifecycleBoard key={version} onOpenCompany={openCompany} />;
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<ScreenLayout
|
|
134
|
+
title="Lifecycle"
|
|
135
|
+
description="Follow every company from suspect to customer, and win back the ones that left."
|
|
136
|
+
icon={<GitBranch className="size-5" />}
|
|
137
|
+
actions={toolbarActions}
|
|
138
|
+
tabs={[{ id: "board", label: "Board" }, ...stages.map((s) => ({ id: s.slug, label: s.name }))]}
|
|
139
|
+
activeTab={tab}
|
|
140
|
+
onTabChange={(id) => { setTab(id); close(); }}
|
|
141
|
+
stats={stages.map((s) => ({ label: s.name, value: countOf(s) == null ? "-" : (countOf(s) as number).toLocaleString() }))}
|
|
142
|
+
>
|
|
143
|
+
<SplitView detailOpen={Boolean(record)} onCloseDetail={close} expanded={mode === "full" && Boolean(record)} onExpandedChange={(full) => setMode(full ? "full" : null)} detailWidth="xl" detail={detail} list={body} />
|
|
144
|
+
{moving ? (
|
|
145
|
+
<StageMoveDialog kind={moving.record.kind} recordId={moving.record.id} recordName={moving.name} stages={stages} onClose={() => setMoving(null)} onMoved={() => { setMoving(null); refresh(); }} />
|
|
146
|
+
) : null}
|
|
147
|
+
</ScreenLayout>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -85,7 +85,7 @@ export function money(cents: number | null | undefined, currency = "USD"): strin
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function contactName(c: ContactRecord): string {
|
|
88
|
-
return [c.first_name, c.last_name].filter(Boolean).join(" ") ||
|
|
88
|
+
return [c.first_name, c.last_name].filter(Boolean).join(" ") || (c.id != null ? `#${c.id}` : "-");
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** A masked field arrives as `"***"`; the real value needs an unlock (billable) or an enrichment. */
|
package/blocks/lists/README.md
CHANGED
|
@@ -6,6 +6,19 @@ It builds on the `crm` block (the members panel is a `ContactTable`).
|
|
|
6
6
|
|
|
7
7
|
Install: `npx @graph8/sdk add lists` (installs `foundation` and `crm` first).
|
|
8
8
|
|
|
9
|
+
## Screens
|
|
10
|
+
|
|
11
|
+
Two finished pages. Mount one as a route with no props (`<ListsScreen />`); both take optional `toolbarActions`,
|
|
12
|
+
`renderRowActions` and `renderDetailActions` slots for cross-block glue.
|
|
13
|
+
|
|
14
|
+
| Screen | What it does |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `ListsScreen` | Searchable, paged table of lists with member counts and build status; **New list** / Edit / Delete (confirmed) in a side sheet; open a list (`?id=`) for its Members tab (contacts, remove from list with confirmation), **Add contacts** from the CRM (conflicts shown, never auto-picked), **Refresh** for dynamic lists, **Check health**, and a Details tab. **Export** downloads a list as CSV or XLSX. `?view=full` opens a list or a suppressed contact full-page. Company lists show counts; the API has no read operation for company-list members, so they link out to the graph8 app. |
|
|
17
|
+
| `SuppressionsScreen` | Do-not-contact ledger filtered by channel and reason; **Suppress contacts** by id or by ticking people from the CRM (default channel: all); check any contact id; open a row for the contact's record and live status; **Reinstate** (confirmed, lifts every suppression on the contact). |
|
|
18
|
+
|
|
19
|
+
Prerequisites: `crm` installed (the CLI does it), proxy allowlist `lists` and `contacts`, key scopes `lists:read/write/delete`
|
|
20
|
+
and `contacts:read/write`. The screens use the foundation screen kit (`components/screen/*`, `hooks/use-url-state`).
|
|
21
|
+
|
|
9
22
|
## What you get
|
|
10
23
|
|
|
11
24
|
| Component | Use it for | Embed props |
|
package/blocks/lists/block.json
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"members panel built on the CRM ContactTable with remove-from-list",
|
|
9
9
|
"ListPicker (search or create) and AddToListButton for any contact selection",
|
|
10
10
|
"conflict review when contacts belong to other reps",
|
|
11
|
-
"suppression ledger by channel and reason, bulk suppress, check, reinstate"
|
|
11
|
+
"suppression ledger by channel and reason, bulk suppress, check, reinstate",
|
|
12
|
+
"finished ListsScreen and SuppressionsScreen pages"
|
|
12
13
|
],
|
|
13
14
|
"dependsOn": [
|
|
14
15
|
"crm"
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"check_list_integrity_lists__list_id__integrity_check_post",
|
|
21
22
|
"create_list_lists_post",
|
|
22
23
|
"delete_list_lists__list_id__delete",
|
|
24
|
+
"download_list_lists__audience_id__download_get",
|
|
23
25
|
"list_contact_fields_fields_get",
|
|
24
26
|
"list_contacts_contacts_get",
|
|
25
27
|
"list_lists_lists_get",
|
|
@@ -90,6 +92,41 @@
|
|
|
90
92
|
"read",
|
|
91
93
|
"write"
|
|
92
94
|
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "ListsScreen",
|
|
98
|
+
"file": "components/lists-screen.tsx",
|
|
99
|
+
"purpose": "Finished Lists page: searchable table, create/edit/delete, member panel with add and remove, refresh and health check.",
|
|
100
|
+
"operations": [
|
|
101
|
+
"check_list_integrity_lists__list_id__integrity_check_post",
|
|
102
|
+
"create_list_lists_post",
|
|
103
|
+
"delete_list_lists__list_id__delete",
|
|
104
|
+
"download_list_lists__audience_id__download_get",
|
|
105
|
+
"list_lists_lists_get",
|
|
106
|
+
"refresh_dynamic_list_lists__audience_id__refresh_post",
|
|
107
|
+
"remove_contacts_from_list_lists__list_id__contacts_delete",
|
|
108
|
+
"update_list_lists__list_id__patch"
|
|
109
|
+
],
|
|
110
|
+
"tiers": [
|
|
111
|
+
"read",
|
|
112
|
+
"write",
|
|
113
|
+
"destructive"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "SuppressionsScreen",
|
|
118
|
+
"file": "components/suppressions-screen.tsx",
|
|
119
|
+
"purpose": "Finished do-not-contact page: filtered ledger, suppress by id or from the CRM, contact detail, confirmed reinstate.",
|
|
120
|
+
"operations": [
|
|
121
|
+
"bulk_add_suppressions_contacts_suppressions_bulk_add_post",
|
|
122
|
+
"check_contact_suppression_contacts__contact_id__suppression_get",
|
|
123
|
+
"list_suppressions_contacts_suppressions_get",
|
|
124
|
+
"reinstate_suppressions_contacts_suppressions_reinstate_post"
|
|
125
|
+
],
|
|
126
|
+
"tiers": [
|
|
127
|
+
"read",
|
|
128
|
+
"write"
|
|
129
|
+
]
|
|
93
130
|
}
|
|
94
131
|
],
|
|
95
132
|
"hooks": [],
|
|
@@ -161,6 +198,7 @@
|
|
|
161
198
|
}
|
|
162
199
|
],
|
|
163
200
|
"peerDependencies": {
|
|
201
|
+
"lucide-react": ">=0.400.0",
|
|
164
202
|
"react": ">=18"
|
|
165
203
|
}
|
|
166
204
|
}
|
|
@@ -21,6 +21,7 @@ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogH
|
|
|
21
21
|
import { ApiError } from "@/g8/components/api-error";
|
|
22
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
23
|
import { g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
import { plural } from "@/g8/lib/plural";
|
|
24
25
|
import { ListPicker } from "./list-picker";
|
|
25
26
|
import type { ListSummary } from "./list-picker";
|
|
26
27
|
|
|
@@ -63,7 +64,7 @@ export function AddToListButton({ contactIds, listId, label = "Add to list", var
|
|
|
63
64
|
...(resolution ? { conflict_resolution: resolution } : {}),
|
|
64
65
|
});
|
|
65
66
|
return { listId: destination as number, added: res.data.added, already_member: res.data.already_member ?? [], not_found: res.data.not_found ?? [] };
|
|
66
|
-
}, { onSuccess: (r) => { setResult(r); onAdded?.(r); } });
|
|
67
|
+
}, { invalidates: [["lists"], ["contacts", "list"]], onSuccess: (r) => { setResult(r); onAdded?.(r); } });
|
|
67
68
|
|
|
68
69
|
const conflict = conflictOf(add.error);
|
|
69
70
|
const close = () => { setOpen(false); setResult(null); setTarget(null); add.reset(); };
|
|
@@ -74,7 +75,7 @@ export function AddToListButton({ contactIds, listId, label = "Add to list", var
|
|
|
74
75
|
<Dialog open={open} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
|
75
76
|
<DialogContent>
|
|
76
77
|
<DialogHeader>
|
|
77
|
-
<DialogTitle>Add {
|
|
78
|
+
<DialogTitle>Add {plural(contactIds.length, "contact")} to a list</DialogTitle>
|
|
78
79
|
<DialogDescription>Contacts already in the list are skipped.</DialogDescription>
|
|
79
80
|
</DialogHeader>
|
|
80
81
|
|
|
@@ -31,6 +31,9 @@ export interface ListSummary {
|
|
|
31
31
|
contact_count?: number | null;
|
|
32
32
|
company_count?: number | null;
|
|
33
33
|
is_dynamic?: boolean;
|
|
34
|
+
source?: string | null;
|
|
35
|
+
created_by?: string | null;
|
|
36
|
+
tags?: string[];
|
|
34
37
|
created_at?: string | null;
|
|
35
38
|
updated_at?: string | null;
|
|
36
39
|
}
|
|
@@ -60,8 +63,9 @@ export function ListPicker({ value, onChange, type = "contacts", allowCreate = t
|
|
|
60
63
|
const [search, setSearch] = useState("");
|
|
61
64
|
const [newName, setNewName] = useState(suggestedName ?? "");
|
|
62
65
|
const q = useDebounced(search.trim());
|
|
63
|
-
const lists = useG8Query((signal) => g8Get<ListSummary[]>("/lists", { search: q || undefined, limit: 50 }, { signal }), [q]);
|
|
66
|
+
const lists = useG8Query((signal) => g8Get<ListSummary[]>("/lists", { search: q || undefined, limit: 50 }, { signal }), [q], { key: ["lists", "picker", { q }] });
|
|
64
67
|
const create = useG8Mutation((title: string) => g8Post<ListSummary>("/lists", { title, type }), {
|
|
68
|
+
invalidates: [["lists"]],
|
|
65
69
|
onSuccess: (res) => { setNewName(""); void lists.refetch(); onChange({ type, ...res.data, title: res.data.title ?? newName }); },
|
|
66
70
|
});
|
|
67
71
|
|
|
@@ -27,9 +27,13 @@ import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
|
27
27
|
import { ContactTable } from "@/g8/crm/components/contact-table";
|
|
28
28
|
import { useDebouncedValue } from "@/g8/crm/hooks/use-debounced";
|
|
29
29
|
import { usePageQuery } from "@/g8/crm/hooks/use-page-query";
|
|
30
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
31
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
30
32
|
import type { ListSummary } from "./list-picker";
|
|
31
33
|
|
|
32
34
|
export interface ListTableProps {
|
|
35
|
+
/** 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. */
|
|
36
|
+
linkParam?: string;
|
|
33
37
|
pageSize?: number;
|
|
34
38
|
/** Called when a list row is clicked. Without it, contact lists open the members panel. */
|
|
35
39
|
onOpen?: (list: ListSummary) => void;
|
|
@@ -44,7 +48,7 @@ function count(l: ListSummary): number {
|
|
|
44
48
|
|
|
45
49
|
const STATUS_STYLE: Record<string, string> = { failed: "text-destructive", processing: "text-amber-600" };
|
|
46
50
|
|
|
47
|
-
export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
|
|
51
|
+
export function ListTable({ linkParam, pageSize = 25, onOpen, allowCreate = true, refreshKey, className }: ListTableProps) {
|
|
48
52
|
const [search, setSearch] = useState("");
|
|
49
53
|
const debouncedSearch = useDebouncedValue(search);
|
|
50
54
|
const [editing, setEditing] = useState<{ id?: number; title: string; description: string; type: "contacts" | "companies" } | null>(null);
|
|
@@ -57,13 +61,15 @@ export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKe
|
|
|
57
61
|
(page, signal) => g8Get<ListSummary[]>("/lists", { page, limit: pageSize, search: debouncedSearch.trim() || undefined }, { signal }),
|
|
58
62
|
[debouncedSearch, refreshKey],
|
|
59
63
|
pageSize,
|
|
64
|
+
["lists", "list", "table"],
|
|
60
65
|
);
|
|
61
66
|
const save = useG8Mutation(
|
|
62
67
|
(v: NonNullable<typeof editing>) => (v.id ? g8Patch(`/lists/${v.id}`, { title: v.title, description: v.description }) : g8Post("/lists", { title: v.title, type: v.type, description: v.description || undefined })),
|
|
63
|
-
{ onSuccess: () => { setEditing(null); void lists.refetch(); } },
|
|
68
|
+
{ invalidates: [["lists"]], onSuccess: () => { setEditing(null); void lists.refetch(); } },
|
|
64
69
|
);
|
|
65
|
-
const remove = useG8Mutation((id: number) => g8Delete(`/lists/${id}`), { onSuccess: () => { setDeleting(null); void lists.refetch(); } });
|
|
70
|
+
const remove = useG8Mutation((id: number) => g8Delete(`/lists/${id}`), { invalidates: [["lists"]], onSuccess: () => { setDeleting(null); void lists.refetch(); } });
|
|
66
71
|
const removeMembers = useG8Mutation((v: { listId: number; ids: number[] }) => g8Delete(`/lists/${v.listId}/contacts`, { body: { contact_ids: v.ids } }), {
|
|
72
|
+
invalidates: [["lists"], ["contacts", "list"]],
|
|
67
73
|
onSuccess: () => { setRemoving(null); setMembersKey((k) => k + 1); void lists.refetch(); },
|
|
68
74
|
});
|
|
69
75
|
|
|
@@ -87,8 +93,8 @@ export function ListTable({ pageSize = 25, onOpen, allowCreate = true, refreshKe
|
|
|
87
93
|
{lists.loading && lists.rows.length === 0
|
|
88
94
|
? Array.from({ length: 4 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={6}><Skeleton className="h-5 w-full" /></TableCell></TableRow>)
|
|
89
95
|
: lists.rows.map((l) => (
|
|
90
|
-
<TableRow key={l.id} data-testid={`list-row-${l.id}`} className="cursor-pointer"
|
|
91
|
-
<TableCell className="font-medium">{l.title}{l.is_dynamic ? <span className="ml-2 text-xs text-muted-foreground">dynamic</span> : null}</TableCell>
|
|
96
|
+
<TableRow key={l.id} data-testid={`list-row-${l.id}`} className="cursor-pointer" {...rowActivationProps(() => open(l))}>
|
|
97
|
+
<TableCell className="font-medium">{onOpen || l.type !== "companies" ? <RecordLink param={linkParam} id={l.id} onOpen={() => open(l)}>{l.title}</RecordLink> : l.title}{l.is_dynamic ? <span className="ml-2 text-xs text-muted-foreground">dynamic</span> : null}</TableCell>
|
|
92
98
|
<TableCell className="capitalize">{l.type ?? "-"}</TableCell>
|
|
93
99
|
<TableCell className="text-right">{count(l).toLocaleString()}</TableCell>
|
|
94
100
|
<TableCell className={STATUS_STYLE[l.status ?? ""] ?? ""} title={l.status_detail ?? undefined}>{l.status_reason ? l.status_reason.replace(/_/g, " ") : l.status ?? "-"}</TableCell>
|