@graph8/sdk 0.303.0 → 0.357.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 +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +2220 -23
- package/dist/index.d.ts +2220 -23
- package/dist/index.js +64 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +2082 -20
- package/dist/react.d.ts +2082 -20
- package/dist/react.js +63 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +63 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +7 -3
- package/docs/api/appObjects.d.ts +3 -1
- package/docs/api/appObjects.md +2 -1
- package/docs/api/by-path.md +51 -0
- package/docs/api/dataPipelines.d.ts +149 -0
- package/docs/api/dataPipelines.md +232 -0
- package/docs/api/forms.d.ts +16 -0
- package/docs/api/forms.md +8 -0
- package/docs/api/objects.d.ts +705 -5
- package/docs/api/objects.md +1099 -14
- package/docs/api/ops.d.ts +1 -1
- package/docs/api/ops.md +1 -1
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/quotes.d.ts +3 -1
- package/docs/api/quotes.md +7 -2
- package/docs/api/radar.d.ts +4 -0
- package/docs/api/radar.md +1 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordForm: create or edit one contact or company, including the org's custom
|
|
5
|
+
* fields. `record` present = edit (only changed fields are sent); absent = create.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order (all `write`, so no confirm dialog):
|
|
8
|
+
* 1. listContactFields | listCompanyFields GET /fields | /fields/companies which custom fields to render
|
|
9
|
+
* 2. createContacts | createCompany POST /contacts | /companies create (custom values ride along on contacts)
|
|
10
|
+
* updateContact | updateCompany PATCH /contacts/{id} | /companies/{id} edit, only changed base fields
|
|
11
|
+
* 3. setFieldValuesBatch PATCH /fields/values/batch custom values (edit, and company create)
|
|
12
|
+
* 4. createField POST /fields optional inline "Add custom field"
|
|
13
|
+
*
|
|
14
|
+
* Creating a contact that already exists answers 409: the form says so and offers
|
|
15
|
+
* `onDuplicate(existingId)` instead of silently creating a second copy.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import type { FormEvent } from "react";
|
|
20
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useCustomFields } from "../hooks/use-custom-fields";
|
|
25
|
+
import { compact, errorDetail, errorStatus, fieldLabel } from "../lib/crm-types";
|
|
26
|
+
import type { CompanyRecord, ContactRecord, FieldDef } from "../lib/crm-types";
|
|
27
|
+
|
|
28
|
+
export interface RecordFormProps {
|
|
29
|
+
entity: "contact" | "company";
|
|
30
|
+
/** Edit this record. Load it with `include_custom_fields=true` to prefill custom values. */
|
|
31
|
+
record?: ContactRecord | CompanyRecord;
|
|
32
|
+
/** Create only: also add the new record to this list. */
|
|
33
|
+
listId?: number;
|
|
34
|
+
onSaved?: (result: { id: number; merged: boolean }) => void;
|
|
35
|
+
/** Create only: a contact with the same identity exists; receives its id. */
|
|
36
|
+
onDuplicate?: (existingId: number | undefined) => void;
|
|
37
|
+
onCancel?: () => void;
|
|
38
|
+
/** Show the inline "Add custom field" control. Default true. */
|
|
39
|
+
allowNewFields?: boolean;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface BaseField { key: string; label: string; required?: boolean; type?: string }
|
|
44
|
+
|
|
45
|
+
const CONTACT_FIELDS: BaseField[] = [
|
|
46
|
+
{ key: "first_name", label: "First name" }, { key: "last_name", label: "Last name" },
|
|
47
|
+
{ key: "work_email", label: "Work email", type: "email" }, { key: "job_title", label: "Job title" },
|
|
48
|
+
{ key: "seniority_level", label: "Seniority" }, { key: "linkedin_url", label: "LinkedIn URL", type: "url" },
|
|
49
|
+
{ key: "direct_phone", label: "Direct phone", type: "tel" }, { key: "mobile_phone", label: "Mobile phone", type: "tel" },
|
|
50
|
+
{ key: "city", label: "City" }, { key: "state", label: "State" }, { key: "country", label: "Country" },
|
|
51
|
+
{ key: "company_domain", label: "Company domain" },
|
|
52
|
+
];
|
|
53
|
+
const COMPANY_FIELDS: BaseField[] = [
|
|
54
|
+
{ key: "domain", label: "Domain", required: true }, { key: "name", label: "Name" },
|
|
55
|
+
{ key: "website", label: "Website", type: "url" }, { key: "industry", label: "Industry" },
|
|
56
|
+
{ key: "employee_count", label: "Employees (e.g. 51-200)" }, { key: "phone", label: "Phone", type: "tel" },
|
|
57
|
+
{ key: "linkedin_url", label: "LinkedIn URL", type: "url" }, { key: "city", label: "City" },
|
|
58
|
+
{ key: "state", label: "State" }, { key: "country", label: "Country" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
type Values = Record<string, string>;
|
|
62
|
+
|
|
63
|
+
function initialValues(base: BaseField[], record: RecordFormProps["record"], custom: FieldDef[]): Values {
|
|
64
|
+
const source = (record ?? {}) as unknown as Record<string, unknown>;
|
|
65
|
+
const out: Values = {};
|
|
66
|
+
for (const f of base) out[f.key] = typeof source[f.key] === "string" && source[f.key] !== "***" ? (source[f.key] as string) : "";
|
|
67
|
+
for (const f of custom) out[`cf:${f.name}`] = (record?.custom_fields?.[f.name ?? ""] ?? "") as string;
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function CustomInput({ field, value, onChange }: { field: FieldDef; value: string; onChange: (v: string) => void }) {
|
|
72
|
+
const id = `cf-${field.id}`;
|
|
73
|
+
return (
|
|
74
|
+
<div className="flex flex-col gap-1">
|
|
75
|
+
<Label htmlFor={id}>{fieldLabel(field)}</Label>
|
|
76
|
+
{field.data_type === "boolean" ? (
|
|
77
|
+
<select id={id} className="h-9 rounded-md border bg-background px-2 text-sm" value={value} onChange={(e) => onChange(e.target.value)}>
|
|
78
|
+
<option value="">Not set</option><option value="true">Yes</option><option value="false">No</option>
|
|
79
|
+
</select>
|
|
80
|
+
) : (
|
|
81
|
+
<Input id={id} type={field.data_type === "int" || field.data_type === "numeric" ? "number" : "text"} value={value} onChange={(e) => onChange(e.target.value)} />
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function RecordForm(props: RecordFormProps) {
|
|
88
|
+
const { entity, record, listId } = props;
|
|
89
|
+
const plural = entity === "contact" ? "contacts" : "companies";
|
|
90
|
+
const base = entity === "contact" ? CONTACT_FIELDS : COMPANY_FIELDS;
|
|
91
|
+
const custom = useCustomFields(plural, listId);
|
|
92
|
+
const [edits, setEdits] = useState<Values>({});
|
|
93
|
+
const [newTitle, setNewTitle] = useState("");
|
|
94
|
+
const [newType, setNewType] = useState("text");
|
|
95
|
+
const [validation, setValidation] = useState<string | null>(null);
|
|
96
|
+
|
|
97
|
+
const initial = initialValues(base, record, custom.fields);
|
|
98
|
+
const value = (key: string) => edits[key] ?? initial[key] ?? "";
|
|
99
|
+
const set = (key: string, v: string) => setEdits((e) => ({ ...e, [key]: v }));
|
|
100
|
+
|
|
101
|
+
const addField = useG8Mutation(
|
|
102
|
+
(title: string) => g8Post("/fields", { title, data_type: newType, entity: plural, list_id: listId ?? null }),
|
|
103
|
+
{ onSuccess: () => { setNewTitle(""); void custom.refetch(); } },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const save = useG8Mutation(async () => {
|
|
107
|
+
const baseValues = Object.fromEntries(base.map((f) => [f.key, value(f.key)]));
|
|
108
|
+
const customChanges = custom.fields.filter((f) => `cf:${f.name}` in edits);
|
|
109
|
+
let id = record?.id;
|
|
110
|
+
let merged = false;
|
|
111
|
+
if (record) {
|
|
112
|
+
const changed = Object.fromEntries(base.filter((f) => f.key in edits).map((f) => [f.key, edits[f.key]]));
|
|
113
|
+
if (Object.keys(changed).length) await g8Patch(`/${plural}/${record.id}`, changed);
|
|
114
|
+
} else if (entity === "contact") {
|
|
115
|
+
const customFields = Object.fromEntries(custom.fields.map((f) => [f.name!, value(`cf:${f.name}`)]).filter(([, v]) => v !== ""));
|
|
116
|
+
const created = await g8Post<{ contact_id?: number; merged?: boolean }>("/contacts", { ...compact(baseValues), list_id: listId, ...(Object.keys(customFields).length ? { custom_fields: customFields } : {}) });
|
|
117
|
+
id = created.data.contact_id;
|
|
118
|
+
merged = Boolean(created.data.merged);
|
|
119
|
+
} else {
|
|
120
|
+
const created = await g8Post<{ company_id?: number | null; merged?: boolean }>("/companies", { ...compact(baseValues), list_id: listId });
|
|
121
|
+
id = created.data.company_id ?? undefined;
|
|
122
|
+
merged = Boolean(created.data.merged);
|
|
123
|
+
}
|
|
124
|
+
const toWrite = record ? customChanges : custom.fields.filter((f) => entity === "company" && value(`cf:${f.name}`) !== "");
|
|
125
|
+
if (id != null && toWrite.length) {
|
|
126
|
+
await g8Patch("/fields/values/batch", { entity: plural, rows: [{ record_id: id, fields: toWrite.map((f) => ({ column_id: f.id, value: value(`cf:${f.name}`) || null })) }] });
|
|
127
|
+
}
|
|
128
|
+
return { id: id as number, merged };
|
|
129
|
+
}, { onSuccess: (r) => props.onSaved?.(r) });
|
|
130
|
+
|
|
131
|
+
const submit = (event: FormEvent) => {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
const missing = base.find((f) => f.required && !value(f.key).trim());
|
|
134
|
+
if (missing) return setValidation(`${missing.label} is required.`);
|
|
135
|
+
if (!record && entity === "contact" && !["first_name", "last_name", "work_email", "linkedin_url"].some((k) => value(k).trim())) {
|
|
136
|
+
return setValidation("Enter a name, an email or a LinkedIn URL.");
|
|
137
|
+
}
|
|
138
|
+
if (value("work_email") && entity === "contact" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value("work_email"))) return setValidation("Work email is not a valid address.");
|
|
139
|
+
setValidation(null);
|
|
140
|
+
void save.mutate(undefined);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const duplicate = !record && errorStatus(save.error) === 409;
|
|
144
|
+
const existingId = (errorDetail(save.error)?.update as { contact_id?: number } | null | undefined)?.contact_id;
|
|
145
|
+
const dirty = Object.keys(edits).length > 0;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<form noValidate onSubmit={submit} className={`flex flex-col gap-4 ${props.className ?? ""}`} aria-label={record ? `Edit ${entity}` : `New ${entity}`}>
|
|
149
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
150
|
+
{base.map((f) => (
|
|
151
|
+
<div key={f.key} className="flex flex-col gap-1">
|
|
152
|
+
<Label htmlFor={`f-${f.key}`}>{f.label}{f.required ? " *" : ""}</Label>
|
|
153
|
+
<Input id={`f-${f.key}`} type={f.type ?? "text"} value={value(f.key)} onChange={(e) => set(f.key, e.target.value)} />
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{custom.fields.length > 0 ? (
|
|
159
|
+
<fieldset className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
160
|
+
<legend className="mb-1 text-sm font-medium">Custom fields</legend>
|
|
161
|
+
{custom.fields.map((f) => <CustomInput key={f.id} field={f} value={value(`cf:${f.name}`)} onChange={(v) => set(`cf:${f.name}`, v)} />)}
|
|
162
|
+
</fieldset>
|
|
163
|
+
) : null}
|
|
164
|
+
{custom.error ? <ApiError error={custom.error} title="Could not load custom fields" onRetry={() => void custom.refetch()} /> : null}
|
|
165
|
+
|
|
166
|
+
{props.allowNewFields !== false ? (
|
|
167
|
+
<div className="flex flex-wrap items-end gap-2">
|
|
168
|
+
<div className="flex flex-col gap-1"><Label htmlFor="new-field-title">New custom field</Label><Input id="new-field-title" placeholder="Field name" value={newTitle} onChange={(e) => setNewTitle(e.target.value)} /></div>
|
|
169
|
+
<select aria-label="Field type" className="h-9 rounded-md border bg-background px-2 text-sm" value={newType} onChange={(e) => setNewType(e.target.value)}>
|
|
170
|
+
<option value="text">Text</option><option value="int">Whole number</option><option value="numeric">Decimal</option><option value="boolean">Yes/no</option>
|
|
171
|
+
</select>
|
|
172
|
+
<Button type="button" variant="outline" size="sm" disabled={!newTitle.trim()} loading={addField.loading} onClick={() => void addField.mutate(newTitle.trim())}>Add field</Button>
|
|
173
|
+
</div>
|
|
174
|
+
) : null}
|
|
175
|
+
{addField.error ? <ApiError error={addField.error} title="Could not add the field" /> : null}
|
|
176
|
+
|
|
177
|
+
{validation ? <p role="alert" className="text-sm text-destructive">{validation}</p> : null}
|
|
178
|
+
{duplicate ? (
|
|
179
|
+
<div role="alert" className="rounded-md border border-amber-300 bg-amber-50 p-3 text-sm">
|
|
180
|
+
A contact with this email or LinkedIn URL already exists{existingId ? ` (id ${existingId})` : ""}.
|
|
181
|
+
{props.onDuplicate ? <Button type="button" variant="link" size="sm" onClick={() => props.onDuplicate?.(existingId)}>Open the existing contact</Button> : null}
|
|
182
|
+
</div>
|
|
183
|
+
) : save.error ? <ApiError error={save.error} title="Could not save" /> : null}
|
|
184
|
+
|
|
185
|
+
<div className="flex justify-end gap-2">
|
|
186
|
+
{props.onCancel ? <Button type="button" variant="ghost" onClick={props.onCancel}>Cancel</Button> : null}
|
|
187
|
+
<Button type="submit" loading={save.loading} disabled={Boolean(record) && !dirty}>{record ? "Save changes" : `Create ${entity}`}</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</form>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TaskList: open and completed tasks on a contact or company, with quick add,
|
|
5
|
+
* complete/reopen and delete.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listContactTasks | listCompanyTasks GET /contacts/{id}/tasks | /companies/{id}/tasks (read)
|
|
9
|
+
* 2. createTask | createCompanyTask POST same path (write)
|
|
10
|
+
* 3. updateTask PATCH /tasks/{task_id} status open|completed (write)
|
|
11
|
+
* 4. deleteTask DELETE /tasks/{task_id} (destructive, ConfirmSpendDialog)
|
|
12
|
+
*
|
|
13
|
+
* Priority: 1 high, 2 medium, 3 low, 0 none.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Button, Input, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
20
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
24
|
+
|
|
25
|
+
export interface TaskItem {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
description?: string | null;
|
|
29
|
+
due_date?: string | null;
|
|
30
|
+
status?: string | null;
|
|
31
|
+
priority?: number | null;
|
|
32
|
+
assignee_name?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TaskListProps {
|
|
36
|
+
entity: "contact" | "company";
|
|
37
|
+
entityId: number;
|
|
38
|
+
/** Show completed tasks too. Default false. */
|
|
39
|
+
includeCompleted?: boolean;
|
|
40
|
+
onChanged?: () => void;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PRIORITY = ["", "High", "Medium", "Low", "Low"];
|
|
45
|
+
|
|
46
|
+
export function TaskList({ entity, entityId, includeCompleted = false, onChanged, className }: TaskListProps) {
|
|
47
|
+
const base = `/${entity === "contact" ? "contacts" : "companies"}/${entityId}/tasks`;
|
|
48
|
+
const [title, setTitle] = useState("");
|
|
49
|
+
const [due, setDue] = useState("");
|
|
50
|
+
const [priority, setPriority] = useState(2);
|
|
51
|
+
const [showDone, setShowDone] = useState(includeCompleted);
|
|
52
|
+
const [deleting, setDeleting] = useState<TaskItem | null>(null);
|
|
53
|
+
|
|
54
|
+
const tasks = useG8Query((signal) => g8Get<TaskItem[]>(base, { status: showDone ? undefined : "open" }, { signal }), [base, showDone]);
|
|
55
|
+
const done = () => { void tasks.refetch(); onChanged?.(); };
|
|
56
|
+
const add = useG8Mutation(
|
|
57
|
+
() => g8Post(base, { title: title.trim(), priority, ...(due ? { due_date: due } : {}) }),
|
|
58
|
+
{ onSuccess: () => { setTitle(""); setDue(""); done(); } },
|
|
59
|
+
);
|
|
60
|
+
const toggle = useG8Mutation((t: TaskItem) => g8Patch(`/tasks/${t.id}`, { status: t.status === "completed" ? "open" : "completed" }), { onSuccess: done });
|
|
61
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/tasks/${id}`), { onSuccess: () => { setDeleting(null); done(); } });
|
|
62
|
+
|
|
63
|
+
const items = (tasks.data?.data ?? []) as TaskItem[];
|
|
64
|
+
return (
|
|
65
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
66
|
+
<form className="flex flex-wrap items-end gap-2" onSubmit={(e) => { e.preventDefault(); if (title.trim()) void add.mutate(undefined); }}>
|
|
67
|
+
<Input aria-label="Task title" placeholder="Add a task" className="min-w-48 flex-1" value={title} onChange={(e) => setTitle(e.target.value)} />
|
|
68
|
+
<Input aria-label="Due date" type="date" className="w-40" value={due} onChange={(e) => setDue(e.target.value)} />
|
|
69
|
+
<select aria-label="Priority" className="h-9 rounded-md border bg-background px-2 text-sm" value={priority} onChange={(e) => setPriority(Number(e.target.value))}>
|
|
70
|
+
<option value={1}>High</option><option value={2}>Medium</option><option value={3}>Low</option><option value={0}>None</option>
|
|
71
|
+
</select>
|
|
72
|
+
<Button type="submit" size="sm" loading={add.loading} disabled={!title.trim()}>Add task</Button>
|
|
73
|
+
</form>
|
|
74
|
+
<label className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
75
|
+
<input type="checkbox" checked={showDone} onChange={(e) => setShowDone(e.target.checked)} /> Show completed
|
|
76
|
+
</label>
|
|
77
|
+
|
|
78
|
+
{add.error ? <ApiError error={add.error} title="Could not add the task" /> : null}
|
|
79
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not update the task" /> : null}
|
|
80
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the task" /> : null}
|
|
81
|
+
{tasks.error ? <ApiError error={tasks.error} title="Could not load tasks" onRetry={() => void tasks.refetch()} /> : null}
|
|
82
|
+
{tasks.loading && !tasks.data ? <Skeleton className="h-12 w-full" /> : null}
|
|
83
|
+
{!tasks.loading && !tasks.error && items.length === 0 ? <EmptyState title="No tasks" description="Add a follow-up so nothing slips." /> : null}
|
|
84
|
+
|
|
85
|
+
<ul className="flex flex-col gap-2">
|
|
86
|
+
{items.map((t) => {
|
|
87
|
+
const completed = t.status === "completed";
|
|
88
|
+
return (
|
|
89
|
+
<li key={t.id} className="flex items-center gap-3 rounded-md border p-3" data-testid="task-item">
|
|
90
|
+
<input type="checkbox" aria-label={`${completed ? "Reopen" : "Complete"} ${t.title}`} checked={completed} onChange={() => void toggle.mutate(t)} />
|
|
91
|
+
<div className="min-w-0 flex-1">
|
|
92
|
+
<p className={`truncate text-sm ${completed ? "text-muted-foreground line-through" : "font-medium"}`}>{t.title}</p>
|
|
93
|
+
<p className="text-xs text-muted-foreground">
|
|
94
|
+
{[t.due_date ? `Due ${new Date(t.due_date).toLocaleDateString(undefined, { timeZone: "UTC" })}` : null, PRIORITY[t.priority ?? 0] || null, t.assignee_name].filter(Boolean).join(" - ")}
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
<Button variant="ghost" size="sm" onClick={() => setDeleting(t)}>Delete</Button>
|
|
98
|
+
</li>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</ul>
|
|
102
|
+
|
|
103
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title="Delete this task" description={deleting?.title} affected={1}
|
|
104
|
+
confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
4
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
5
|
+
import { isCustomField } from "../lib/crm-types";
|
|
6
|
+
import type { CrmEntity, FieldDef } from "../lib/crm-types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The org's custom columns for contacts or companies (`GET /fields`,
|
|
10
|
+
* `GET /fields/companies`). `fields` are the custom ones only, in API order;
|
|
11
|
+
* `all` includes the base columns. Pass `listId` to also get list-scoped columns. `enabled: false` makes no request.
|
|
12
|
+
*/
|
|
13
|
+
export function useCustomFields(entity: CrmEntity, listId?: number, enabled = true) {
|
|
14
|
+
const path = entity === "contacts" ? "/fields" : "/fields/companies";
|
|
15
|
+
const query = useG8Query((signal) => g8Get<FieldDef[]>(path, { list_id: listId }, { signal }), [path, listId], { enabled });
|
|
16
|
+
const all = (query.data?.data ?? []) as FieldDef[];
|
|
17
|
+
return { fields: all.filter(isCustomField), all, loading: query.loading, error: query.error, refetch: query.refetch };
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/** `value`, but only after it stopped changing for `ms`. Keeps filter boxes from firing a request per keystroke. */
|
|
6
|
+
export function useDebouncedValue<T>(value: T, ms = 300): T {
|
|
7
|
+
const [v, setV] = useState(value);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const t = setTimeout(() => setV(value), ms);
|
|
10
|
+
return () => clearTimeout(t);
|
|
11
|
+
}, [value, ms]);
|
|
12
|
+
return v;
|
|
13
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
5
|
+
import type { G8Envelope } from "@/g8/lib/g8-client";
|
|
6
|
+
import type { PageInfo } from "../lib/crm-types";
|
|
7
|
+
|
|
8
|
+
export interface UsePageQueryResult<T> {
|
|
9
|
+
rows: T[];
|
|
10
|
+
total: number;
|
|
11
|
+
page: number;
|
|
12
|
+
pageCount: number;
|
|
13
|
+
hasNext: boolean;
|
|
14
|
+
setPage: (page: number) => void;
|
|
15
|
+
loading: boolean;
|
|
16
|
+
error: unknown;
|
|
17
|
+
refetch: () => Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Page-number pagination for list endpoints that answer
|
|
22
|
+
* `{ data: T[], pagination: { page, limit, total, has_next } }` (contacts,
|
|
23
|
+
* companies, lists...). `usePaginated` follows cursors; these endpoints do not
|
|
24
|
+
* have one, so the table keeps a page number instead.
|
|
25
|
+
*
|
|
26
|
+
* Changing `deps` (a filter, a search box) goes back to page 1 without a second request.
|
|
27
|
+
*/
|
|
28
|
+
export function usePageQuery<T>(
|
|
29
|
+
fetchPage: (page: number, signal: AbortSignal) => Promise<G8Envelope<T[]>>,
|
|
30
|
+
deps: readonly unknown[],
|
|
31
|
+
pageSize = 25,
|
|
32
|
+
): UsePageQueryResult<T> {
|
|
33
|
+
const key = JSON.stringify(deps);
|
|
34
|
+
const [state, setState] = useState({ key, page: 1 });
|
|
35
|
+
const page = state.key === key ? state.page : 1;
|
|
36
|
+
const query = useG8Query((signal) => fetchPage(page, signal), [key, page]);
|
|
37
|
+
|
|
38
|
+
const info = query.data?.pagination as Partial<PageInfo> | undefined;
|
|
39
|
+
const rows = (query.data?.data ?? []) as T[];
|
|
40
|
+
const total = info?.total ?? rows.length;
|
|
41
|
+
return {
|
|
42
|
+
rows,
|
|
43
|
+
total,
|
|
44
|
+
page,
|
|
45
|
+
pageCount: Math.max(1, Math.ceil(total / (info?.limit ?? pageSize))),
|
|
46
|
+
hasNext: info?.has_next ?? false,
|
|
47
|
+
setPage: (next) => setState({ key, page: Math.max(1, next) }),
|
|
48
|
+
loading: query.loading,
|
|
49
|
+
error: query.error,
|
|
50
|
+
refetch: query.refetch,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared shapes and tiny helpers for the CRM block. Types only describe the
|
|
5
|
+
* fields the components read; the API may return more.
|
|
6
|
+
*
|
|
7
|
+
* Masked fields: search results and credit-locked records send "***" instead of
|
|
8
|
+
* a real email/phone. `isMasked` is the single check every component uses, so a
|
|
9
|
+
* masked value is never rendered, copied or sent as if it were real.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface PageInfo {
|
|
13
|
+
page: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
total: number;
|
|
16
|
+
has_next: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CompanySummary {
|
|
20
|
+
id?: number | null;
|
|
21
|
+
name?: string | null;
|
|
22
|
+
domain?: string | null;
|
|
23
|
+
industry?: string | null;
|
|
24
|
+
employee_count?: string | null;
|
|
25
|
+
logo_url?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ContactRecord {
|
|
29
|
+
id: number;
|
|
30
|
+
first_name?: string | null;
|
|
31
|
+
last_name?: string | null;
|
|
32
|
+
full_name?: string | null;
|
|
33
|
+
work_email?: string | null;
|
|
34
|
+
direct_phone?: string | null;
|
|
35
|
+
mobile_phone?: string | null;
|
|
36
|
+
job_title?: string | null;
|
|
37
|
+
job_department?: string | null;
|
|
38
|
+
seniority_level?: string | null;
|
|
39
|
+
linkedin_url?: string | null;
|
|
40
|
+
city?: string | null;
|
|
41
|
+
state?: string | null;
|
|
42
|
+
country?: string | null;
|
|
43
|
+
about?: string | null;
|
|
44
|
+
company_id?: number | null;
|
|
45
|
+
company_name?: string | null;
|
|
46
|
+
company_domain?: string | null;
|
|
47
|
+
company?: CompanySummary | null;
|
|
48
|
+
owner_name?: string | null;
|
|
49
|
+
custom_fields?: Record<string, string | null> | null;
|
|
50
|
+
created_at?: string | null;
|
|
51
|
+
updated_at?: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface CompanyRecord {
|
|
55
|
+
id: number;
|
|
56
|
+
name?: string | null;
|
|
57
|
+
domain?: string | null;
|
|
58
|
+
website?: string | null;
|
|
59
|
+
description?: string | null;
|
|
60
|
+
industry?: string | null;
|
|
61
|
+
employee_count?: string | null;
|
|
62
|
+
revenue?: string | null;
|
|
63
|
+
founded_year?: number | null;
|
|
64
|
+
phone?: string | null;
|
|
65
|
+
address?: string | null;
|
|
66
|
+
city?: string | null;
|
|
67
|
+
state?: string | null;
|
|
68
|
+
country?: string | null;
|
|
69
|
+
zip?: string | null;
|
|
70
|
+
linkedin_url?: string | null;
|
|
71
|
+
logo_url?: string | null;
|
|
72
|
+
contact_count?: number | null;
|
|
73
|
+
owner_name?: string | null;
|
|
74
|
+
custom_fields?: Record<string, string | null> | null;
|
|
75
|
+
created_at?: string | null;
|
|
76
|
+
updated_at?: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A column definition from `GET /fields` (contacts) or `GET /fields/companies`. */
|
|
80
|
+
export interface FieldDef {
|
|
81
|
+
id: number | null;
|
|
82
|
+
title: string;
|
|
83
|
+
name: string | null;
|
|
84
|
+
data_type: string;
|
|
85
|
+
label?: string | null;
|
|
86
|
+
is_global?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type CrmEntity = "contacts" | "companies";
|
|
90
|
+
|
|
91
|
+
/** Base columns are named like CONTACT_FIRST_NAME / COMPANY_NAME; custom ones carry a slug (udo_*). */
|
|
92
|
+
export function isCustomField(field: FieldDef): boolean {
|
|
93
|
+
return field.id != null && !/^(CONTACT|COMPANY)_[A-Z0-9_]+$/.test(field.name ?? "");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function fieldLabel(field: FieldDef): string {
|
|
97
|
+
return field.label || field.title;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** True for the "***" placeholder the API sends for a locked email or phone. */
|
|
101
|
+
export function isMasked(value: unknown): boolean {
|
|
102
|
+
return typeof value === "string" && value.trim() === "***";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function contactName(c: Pick<ContactRecord, "first_name" | "last_name" | "full_name" | "work_email">): string {
|
|
106
|
+
const joined = [c.first_name, c.last_name].filter(Boolean).join(" ").trim();
|
|
107
|
+
return joined || c.full_name || (c.work_email && !isMasked(c.work_email) ? c.work_email : "") || "Unnamed contact";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function initials(name: string): string {
|
|
111
|
+
return name
|
|
112
|
+
.split(/\s+/)
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.slice(0, 2)
|
|
115
|
+
.map((p) => p[0]?.toUpperCase())
|
|
116
|
+
.join("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function location(r: { city?: string | null; state?: string | null; country?: string | null }): string {
|
|
120
|
+
return [r.city, r.state, r.country].filter(Boolean).join(", ");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Skip empty-string values so a PATCH only sends what the person actually filled. */
|
|
124
|
+
export function compact<T extends Record<string, unknown>>(values: T): Partial<T> {
|
|
125
|
+
return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== "" && v != null)) as Partial<T>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function errorStatus(error: unknown): number | undefined {
|
|
129
|
+
return (error as { status?: number } | null)?.status;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The `detail` object of a G8Error when the API sent one (409 duplicate hints, for example). */
|
|
133
|
+
export function errorDetail(error: unknown): Record<string, unknown> | undefined {
|
|
134
|
+
const detail = (error as { detail?: unknown } | null)?.detail;
|
|
135
|
+
return detail && typeof detail === "object" && !Array.isArray(detail) ? (detail as Record<string, unknown>) : undefined;
|
|
136
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Create a custom field and write values to many records at once
|
|
3
|
+
* @when Adding an org-specific column (a score, a segment) and filling it for a batch of contacts or
|
|
4
|
+
* companies. Field creation is a write; values go in with one batch call (up to 500 rows) instead of one
|
|
5
|
+
* request per record. Field ids come from the create or list response.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
export async function scoreContacts(scores: Array<{ contactId: number; score: number }>) {
|
|
12
|
+
const existing = await g8.fields.listContactFields();
|
|
13
|
+
let field = existing.data.find((f) => f.title === "Fit score");
|
|
14
|
+
if (!field) {
|
|
15
|
+
const created = await g8.fields.create({ title: "Fit score", entity: "contacts", data_type: "int" });
|
|
16
|
+
field = { id: created.id, title: created.title, name: created.name, data_type: created.data_type, is_global: created.is_global };
|
|
17
|
+
}
|
|
18
|
+
if (field.id == null) throw new Error("Fit score has no column id");
|
|
19
|
+
|
|
20
|
+
const columnId = field.id;
|
|
21
|
+
const result = await g8.api.fields.setFieldValuesBatch({
|
|
22
|
+
body: {
|
|
23
|
+
entity: "contacts",
|
|
24
|
+
rows: scores.map((s) => ({ record_id: s.contactId, fields: [{ column_id: columnId, value: String(s.score) }] })),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Review duplicate suggestions and merge one pair
|
|
3
|
+
* @when Cleaning the CRM. A merge is destructive (the secondary record stops existing; undo works for
|
|
4
|
+
* 30 days), so list and preview freely but ask a human before calling `mergeContact`. The suggestion
|
|
5
|
+
* and contact ids come from the list response.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
export async function pendingContactDuplicates() {
|
|
12
|
+
const counts = await g8.api.duplicates.duplicateCounts({ query: { entity_type: "contact" } });
|
|
13
|
+
const page = await g8.api.duplicates.listDuplicates({ query: { entity_type: "contact", status: "pending", limit: 25 } });
|
|
14
|
+
return { counts: counts.data, page: page.data };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Side-by-side comparison and blocker check; writes nothing. */
|
|
18
|
+
export async function previewMerge(keepId: number, dropId: number) {
|
|
19
|
+
const preview = await g8.api.contacts.previewContactMerge({ path: { contact_id: keepId }, query: { secondary: dropId } });
|
|
20
|
+
return preview.data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** DESTRUCTIVE. Only after a person confirmed which record to keep. */
|
|
24
|
+
export async function mergeConfirmed(keepId: number, dropId: number, suggestionId: string) {
|
|
25
|
+
const merged = await g8.api.contacts.mergeContact({
|
|
26
|
+
path: { contact_id: keepId },
|
|
27
|
+
body: { secondary_id: dropId, suggestion_id: suggestionId },
|
|
28
|
+
});
|
|
29
|
+
return merged.data;
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Log a note, create a follow-up task and read the activity feed
|
|
3
|
+
* @when Recording what happened on a contact or company and showing the combined timeline. The feed is
|
|
4
|
+
* one read over notes, tasks, emails, calls, meetings and field changes, newest first.
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
export async function logCallOutcome(contactId: number, summary: string, followUpOn: string) {
|
|
11
|
+
await g8.notes.create(contactId, summary);
|
|
12
|
+
const task = await g8.tasks.create(contactId, { title: "Follow up", due_date: followUpOn, priority: 1 });
|
|
13
|
+
|
|
14
|
+
// Company-level notes work the same way: g8.notes.createForCompany(companyId, text).
|
|
15
|
+
const feed = await g8.api.contacts.getActivityFeed({
|
|
16
|
+
query: { entity_type: "contact", entity_id: String(contactId), types: "note,task,email,call", limit: 20 },
|
|
17
|
+
});
|
|
18
|
+
return { taskId: task.id, feed: feed.data };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function completeTask(taskId: string) {
|
|
22
|
+
return g8.tasks.update(taskId, { status: "completed" });
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Upsert a contact with its company and custom fields, then read it back
|
|
3
|
+
* @when Server-side sync or form handler that must write a person who may already exist. `upsert`
|
|
4
|
+
* matches on work_email, then linkedin_url, so it is safe to retry; `company_domain` links (or creates)
|
|
5
|
+
* the company; `custom_fields` are keyed by the field title or slug. The ids you need next come from
|
|
6
|
+
* the responses: look the contact up by email to get its id, never guess it.
|
|
7
|
+
*/
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
const LIST_ID = Number(process.env.G8_LIST_ID); // a list the contact belongs to (see the lists block)
|
|
13
|
+
|
|
14
|
+
export async function upsertPerson(email: string, name: { first: string; last: string }, domain: string) {
|
|
15
|
+
const result = await g8.contacts.upsert({
|
|
16
|
+
list_id: LIST_ID,
|
|
17
|
+
work_email: email,
|
|
18
|
+
first_name: name.first,
|
|
19
|
+
last_name: name.last,
|
|
20
|
+
company_domain: domain,
|
|
21
|
+
custom_fields: { "Lead source": "website" },
|
|
22
|
+
create_missing_fields: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// upsert answers { action, count }, not the id: find the contact by its (exact, case-insensitive) email.
|
|
26
|
+
const { data } = await g8.contacts.list({ email, limit: 1 });
|
|
27
|
+
const contact = data[0];
|
|
28
|
+
if (!contact) throw new Error(`upserted ${email} (${result.action}) but could not read it back`);
|
|
29
|
+
|
|
30
|
+
// The full record, including custom fields and the linked company.
|
|
31
|
+
const detail = await g8.api.contacts.getContact({ path: { contact_id: contact.id }, query: { include_custom_fields: true } });
|
|
32
|
+
return { action: result.action, contact: detail.data };
|
|
33
|
+
}
|