@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -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-load-more-list.ts +61 -0
- 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-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -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 +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- 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/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- 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 +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- 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 +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- 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 +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- 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 +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- 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 +19 -0
- package/blocks/social-ads/block.json +233 -1
- 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 +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- 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 +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- 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 +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- 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 +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- 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 +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -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/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordEngagementTab: how much a contact or company has engaged: emails, calls, meetings, LinkedIn, website visits and
|
|
5
|
+
* downloads, the last touch, and the pages visited in the last 90 days. Use it as an `extraDetailTabs` entry of
|
|
6
|
+
* ContactsScreen / CompaniesScreen:
|
|
7
|
+
* { id: "engagement", label: "Engagement", render: (c) => <RecordEngagementTab kind="contacts" id={c.id} /> }
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. getContactEngagementSummary GET /contacts/{id}/engagement-summary (read) when kind="contacts"
|
|
11
|
+
* getCompanyEngagementSummary GET /companies/{id}/engagement-summary (read) when kind="companies"
|
|
12
|
+
* 2. getContactSitesVisited GET /contacts/{id}/sites-visited?days=90 (read) when kind="contacts"
|
|
13
|
+
* getCompanySitesVisited GET /companies/{id}/sites-visited?days=90 (read) when kind="companies"
|
|
14
|
+
* Nothing here is billable, external or destructive. The visits list is shown ten at a time with "Show more", so the
|
|
15
|
+
* count in the heading always matches what can be reached.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState } from "react";
|
|
19
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
20
|
+
import { Activity } from "lucide-react";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
26
|
+
|
|
27
|
+
/** The counts both summaries share (the contact one adds LinkedIn and the last touch). */
|
|
28
|
+
interface Summary { emails?: number | null; calls?: number | null; meetings?: number | null; linkedin?: number | null; website_visits?: number | null; downloads?: number | null; contact_count?: number | null; last_contact?: string | null; last_contact_checked?: boolean | null }
|
|
29
|
+
interface Visit { url?: string | null; domain?: string | null; title?: string | null; event_date?: string | null; page_type?: string | null }
|
|
30
|
+
interface Visits { visits?: Visit[] | null; total_visits?: number | null }
|
|
31
|
+
|
|
32
|
+
const STEP = 10;
|
|
33
|
+
const DAYS = 90;
|
|
34
|
+
|
|
35
|
+
function dataOf<T>(env: unknown): T {
|
|
36
|
+
const d = (env as { data?: unknown } | null)?.data;
|
|
37
|
+
return (d && typeof d === "object" ? d : {}) as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function when(iso: string | null | undefined): string {
|
|
41
|
+
const d = iso ? new Date(iso) : null;
|
|
42
|
+
return d && !Number.isNaN(d.getTime()) ? d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const count = (n: number | null | undefined): string => (typeof n === "number" ? n.toLocaleString() : "-");
|
|
46
|
+
|
|
47
|
+
export interface RecordEngagementTabProps {
|
|
48
|
+
kind: "contacts" | "companies";
|
|
49
|
+
id: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function RecordEngagementTab({ kind, id }: RecordEngagementTabProps) {
|
|
53
|
+
const [shown, setShown] = useState(STEP);
|
|
54
|
+
const summary = useG8Query(async (signal) => dataOf<Summary>(await g8Get(`/${kind}/${id}/engagement-summary`, undefined, { signal })), [kind, id]);
|
|
55
|
+
const visits = useG8Query(async (signal) => dataOf<Visits>(await g8Get(`/${kind}/${id}/sites-visited`, { days: DAYS }, { signal })), [kind, id]);
|
|
56
|
+
|
|
57
|
+
if (summary.error) return <ApiError error={summary.error} title="Could not load engagement" onRetry={() => void summary.refetch()} />;
|
|
58
|
+
if (!summary.data) return <Skeleton className="h-32 w-full" aria-label="Loading engagement" />;
|
|
59
|
+
const s = summary.data;
|
|
60
|
+
const items = [
|
|
61
|
+
{ label: "Emails", value: count(s.emails) },
|
|
62
|
+
{ label: "Calls", value: count(s.calls) },
|
|
63
|
+
{ label: "Meetings", value: count(s.meetings) },
|
|
64
|
+
...(kind === "contacts" ? [{ label: "LinkedIn", value: count(s.linkedin) }] : [{ label: "People", value: count(s.contact_count) }]),
|
|
65
|
+
{ label: "Website visits", value: count(s.website_visits) },
|
|
66
|
+
{ label: "Downloads", value: count(s.downloads) },
|
|
67
|
+
...(kind === "contacts" ? [{ label: "Last touch", value: when(s.last_contact) || (s.last_contact_checked === false ? "Not checked" : "No touch yet") }] : []),
|
|
68
|
+
];
|
|
69
|
+
const rows = visits.data?.visits ?? [];
|
|
70
|
+
const total = rows.length;
|
|
71
|
+
const reported = visits.data?.total_visits ?? total;
|
|
72
|
+
return (
|
|
73
|
+
<div className="space-y-4">
|
|
74
|
+
<KeyValueList columns={2} items={items} />
|
|
75
|
+
<section aria-label="Pages visited" className="space-y-2">
|
|
76
|
+
<h3 className="text-sm font-medium">Pages visited, last {DAYS} days</h3>
|
|
77
|
+
{visits.error ? <ApiError error={visits.error} title="Could not load pages visited" onRetry={() => void visits.refetch()} /> : null}
|
|
78
|
+
{!visits.error && !visits.data ? <Skeleton className="h-16 w-full" aria-label="Loading pages visited" /> : null}
|
|
79
|
+
{visits.data && total === 0 ? <EmptyState compact icon={<Activity className="size-8" />} title="No pages visited" description={`No website visits in the last ${DAYS} days.`} /> : null}
|
|
80
|
+
{total > 0 ? (
|
|
81
|
+
<>
|
|
82
|
+
<ul className="divide-y rounded-md border">
|
|
83
|
+
{rows.slice(0, shown).map((v, i) => (
|
|
84
|
+
<li key={`${v.url}-${i}`} className="flex items-center justify-between gap-3 p-3 text-sm">
|
|
85
|
+
<span className="min-w-0">
|
|
86
|
+
<span className="block truncate font-medium">{v.title || v.url || "Page"}</span>
|
|
87
|
+
<span className="block truncate text-xs text-muted-foreground">{[v.domain, v.page_type].filter(Boolean).join(" · ")}</span>
|
|
88
|
+
</span>
|
|
89
|
+
<span className="shrink-0 text-xs text-muted-foreground">{when(v.event_date)}</span>
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ul>
|
|
93
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
94
|
+
<span role="status">Showing {Math.min(shown, total).toLocaleString()} of {total.toLocaleString()} visits</span>
|
|
95
|
+
{shown < total ? <Button size="sm" variant="outline" onClick={() => setShown((n) => n + STEP)}>Show more</Button> : null}
|
|
96
|
+
</div>
|
|
97
|
+
{reported > total ? <p role="note" className="text-xs text-muted-foreground">The API returned {total.toLocaleString()} of {reported.toLocaleString()} visits in this window. Narrow it in the visitors screen to see the rest.</p> : null}
|
|
98
|
+
</>
|
|
99
|
+
) : null}
|
|
100
|
+
</section>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -35,6 +35,8 @@ export interface RecordFormProps {
|
|
|
35
35
|
/** Create only: a contact with the same identity exists; receives its id. */
|
|
36
36
|
onDuplicate?: (existingId: number | undefined) => void;
|
|
37
37
|
onCancel?: () => void;
|
|
38
|
+
/** Create only: prefill base fields, for example `{ company_domain: "acme.com" }`. */
|
|
39
|
+
defaults?: Record<string, string>;
|
|
38
40
|
/** Show the inline "Add custom field" control. Default true. */
|
|
39
41
|
allowNewFields?: boolean;
|
|
40
42
|
className?: string;
|
|
@@ -94,13 +96,13 @@ export function RecordForm(props: RecordFormProps) {
|
|
|
94
96
|
const [newType, setNewType] = useState("text");
|
|
95
97
|
const [validation, setValidation] = useState<string | null>(null);
|
|
96
98
|
|
|
97
|
-
const initial = initialValues(base, record, custom.fields);
|
|
99
|
+
const initial = { ...initialValues(base, record, custom.fields), ...(record ? {} : props.defaults ?? {}) };
|
|
98
100
|
const value = (key: string) => edits[key] ?? initial[key] ?? "";
|
|
99
101
|
const set = (key: string, v: string) => setEdits((e) => ({ ...e, [key]: v }));
|
|
100
102
|
|
|
101
103
|
const addField = useG8Mutation(
|
|
102
104
|
(title: string) => g8Post("/fields", { title, data_type: newType, entity: plural, list_id: listId ?? null }),
|
|
103
|
-
{ onSuccess: () => { setNewTitle(""); void custom.refetch(); } },
|
|
105
|
+
{ invalidates: ["fields"], onSuccess: () => { setNewTitle(""); void custom.refetch(); } },
|
|
104
106
|
);
|
|
105
107
|
|
|
106
108
|
const save = useG8Mutation(async () => {
|
|
@@ -126,7 +128,7 @@ export function RecordForm(props: RecordFormProps) {
|
|
|
126
128
|
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
129
|
}
|
|
128
130
|
return { id: id as number, merged };
|
|
129
|
-
}, { onSuccess: (r) => props.onSaved?.(r) });
|
|
131
|
+
}, { invalidates: [plural], onSuccess: (r) => props.onSaved?.(r) });
|
|
130
132
|
|
|
131
133
|
const submit = (event: FormEvent) => {
|
|
132
134
|
event.preventDefault();
|
|
@@ -176,7 +178,7 @@ export function RecordForm(props: RecordFormProps) {
|
|
|
176
178
|
|
|
177
179
|
{validation ? <p role="alert" className="text-sm text-destructive">{validation}</p> : null}
|
|
178
180
|
{duplicate ? (
|
|
179
|
-
<div role="alert" className="rounded-md border border-
|
|
181
|
+
<div role="alert" className="rounded-md border border-warning-foreground/30 bg-warning p-3 text-sm">
|
|
180
182
|
A contact with this email or LinkedIn URL already exists{existingId ? ` (id ${existingId})` : ""}.
|
|
181
183
|
{props.onDuplicate ? <Button type="button" variant="link" size="sm" onClick={() => props.onDuplicate?.(existingId)}>Open the existing contact</Button> : null}
|
|
182
184
|
</div>
|
|
@@ -51,14 +51,14 @@ export function TaskList({ entity, entityId, includeCompleted = false, onChanged
|
|
|
51
51
|
const [showDone, setShowDone] = useState(includeCompleted);
|
|
52
52
|
const [deleting, setDeleting] = useState<TaskItem | null>(null);
|
|
53
53
|
|
|
54
|
-
const tasks = useG8Query((signal) => g8Get<TaskItem[]>(base, { status: showDone ? undefined : "open" }, { signal }), [base, showDone]);
|
|
54
|
+
const tasks = useG8Query((signal) => g8Get<TaskItem[]>(base, { status: showDone ? undefined : "open" }, { signal }), [base, showDone], { key: ["tasks", "record", entity, entityId, { showDone }] });
|
|
55
55
|
const done = () => { void tasks.refetch(); onChanged?.(); };
|
|
56
56
|
const add = useG8Mutation(
|
|
57
57
|
() => g8Post(base, { title: title.trim(), priority, ...(due ? { due_date: due } : {}) }),
|
|
58
|
-
{ onSuccess: () => { setTitle(""); setDue(""); done(); } },
|
|
58
|
+
{ invalidates: [["tasks"]], onSuccess: () => { setTitle(""); setDue(""); done(); } },
|
|
59
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(); } });
|
|
60
|
+
const toggle = useG8Mutation((t: TaskItem) => g8Patch(`/tasks/${t.id}`, { status: t.status === "completed" ? "open" : "completed" }), { invalidates: [["tasks"]], onSuccess: done });
|
|
61
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/tasks/${id}`), { invalidates: [["tasks"]], onSuccess: () => { setDeleting(null); done(); } });
|
|
62
62
|
|
|
63
63
|
const items = (tasks.data?.data ?? []) as TaskItem[];
|
|
64
64
|
return (
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TasksScreen: every task in the workspace in one place, the work queue behind the per-record TaskList.
|
|
5
|
+
* No props are required.
|
|
6
|
+
*
|
|
7
|
+
* Composes: ScreenLayout (with Open / Completed / All tabs), StatStrip, Toolbar, SplitView, DetailPanel, FormSheet
|
|
8
|
+
* (screen kit). It is the only crm screen that lists tasks across records.
|
|
9
|
+
*
|
|
10
|
+
* Flows: switch Open / Completed / All - search by title - filter by priority, type and linked record type - page through
|
|
11
|
+
* results - tick a task to complete or reopen it - open a task (deep link ?id=, full page with &view=full) to read, edit or delete it - create a task
|
|
12
|
+
* (standalone, or linked to a record with `defaultRecord`) - complete or delete many at once ("N of M" report, 404 =
|
|
13
|
+
* already gone) - jump to the linked contact or company through `onOpenRecord`.
|
|
14
|
+
*
|
|
15
|
+
* SDK operations: none of its own. listAllTasks and getTask are `read`; createGlobalTask and updateTask are `write`;
|
|
16
|
+
* deleteTask (detail and bulk) is `destructive` and goes through ConfirmSpendDialog. Priority: 1 high, 2 medium, 3 low, 0 none.
|
|
17
|
+
* Due dates are sent as YYYY-MM-DD and shown in UTC so a date never shifts a day.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect, useState } from "react";
|
|
21
|
+
import type { FormEvent, ReactNode } from "react";
|
|
22
|
+
import { Button, Input, Label, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { CheckCheck, CheckSquare, Plus, Trash2 } from "lucide-react";
|
|
24
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
25
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
26
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
27
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
28
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
29
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
30
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
31
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
32
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
33
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
34
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
35
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
36
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
37
|
+
import { plural } from "@/g8/lib/plural";
|
|
38
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
39
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
40
|
+
import { useUrlFilters } from "@/g8/hooks/use-url-filters";
|
|
41
|
+
import { MoreFilters, TextFilter } from "@/g8/components/screen/filter-controls";
|
|
42
|
+
import { g8Delete, g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
43
|
+
import { usePageQuery } from "../hooks/use-page-query";
|
|
44
|
+
import { errorStatus } from "../lib/crm-types";
|
|
45
|
+
import { Pager, formatDay, useBulkDelete, useRowSelection } from "../lib/screen-parts";
|
|
46
|
+
import type { TaskItem } from "./task-list";
|
|
47
|
+
|
|
48
|
+
/** The fields TasksScreen reads from a task; the API may send more. */
|
|
49
|
+
export interface WorkspaceTask extends TaskItem {
|
|
50
|
+
task_type?: string | null;
|
|
51
|
+
entity_type?: string | null;
|
|
52
|
+
entity_id?: string | null;
|
|
53
|
+
entity_label?: string | null;
|
|
54
|
+
created_at?: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TasksScreenProps {
|
|
58
|
+
/** The signed-in person's user id. With it the toolbar gets a "My tasks" toggle (assignee filter); without it the toggle is hidden. */
|
|
59
|
+
currentUserId?: string;
|
|
60
|
+
pageSize?: number;
|
|
61
|
+
/** Which tab opens first. Default "open". */
|
|
62
|
+
initialStatus?: "open" | "completed" | "all";
|
|
63
|
+
/** New tasks are linked to this record (for example when the screen sits on a contact page). */
|
|
64
|
+
defaultRecord?: { entity_type: "contact" | "company" | "deal"; entity_id: string };
|
|
65
|
+
/** Called to open the linked record of a task ("Open contact"). Without it the link is plain text. */
|
|
66
|
+
onOpenRecord?: (record: { entity_type: string; entity_id: string }) => void;
|
|
67
|
+
renderRowActions?: (task: WorkspaceTask) => ReactNode;
|
|
68
|
+
renderDetailActions?: (task: WorkspaceTask) => ReactNode;
|
|
69
|
+
toolbarActions?: ReactNode;
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PRIORITY: Record<number, string> = { 1: "High", 2: "Medium", 3: "Low", 4: "Low" };
|
|
74
|
+
const TASK_TYPES = ["call", "email", "meeting", "todo"];
|
|
75
|
+
const ENTITY_TYPES = ["contact", "company", "deal"];
|
|
76
|
+
type Tab = "open" | "completed" | "all";
|
|
77
|
+
|
|
78
|
+
function ymd(date = new Date()): string {
|
|
79
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
80
|
+
}
|
|
81
|
+
const isOverdue = (t: WorkspaceTask): boolean => t.status !== "completed" && Boolean(t.due_date) && t.due_date!.slice(0, 10) < ymd();
|
|
82
|
+
/** `pagination.total` of a list answer (the typed envelope only declares the cursor fields). */
|
|
83
|
+
const totalOf = (res?: { pagination?: unknown }): number | undefined => (res?.pagination as { total?: number } | undefined)?.total;
|
|
84
|
+
const label = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
|
|
85
|
+
|
|
86
|
+
function PriorityPill({ priority }: { priority?: number | null }) {
|
|
87
|
+
if (!priority) return <span className="text-muted-foreground">-</span>;
|
|
88
|
+
return <StatusPill tone={priority === 1 ? "negative" : priority === 2 ? "warning" : "neutral"}>{PRIORITY[priority] ?? "Low"}</StatusPill>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every filter the tasks API takes besides the status tab: search, priority, type, linked record type, assignee, creator, source meeting; `mine` = assigned to the signed-in person. */
|
|
92
|
+
const FILTER_KEYS = ["search", "priority", "task_type", "entity_type", "assignee_id", "created_by", "source_meeting_id", "mine"] as const;
|
|
93
|
+
type FilterKey = (typeof FILTER_KEYS)[number];
|
|
94
|
+
|
|
95
|
+
export function TasksScreen(props: TasksScreenProps) {
|
|
96
|
+
const { pageSize = 25 } = props;
|
|
97
|
+
const [idParam, setIdParamRaw] = useUrlState("id");
|
|
98
|
+
const [view, setView] = useUrlState("view");
|
|
99
|
+
const full = view === "full" && idParam != null && idParam !== "";
|
|
100
|
+
/** Changing or clearing the open record also leaves the full-page view. */
|
|
101
|
+
const setIdParam = (v: string | null): void => { if (v == null) setView(null); setIdParamRaw(v); };
|
|
102
|
+
const openId = idParam || null;
|
|
103
|
+
const [tab, setTab] = useState<Tab>(props.initialStatus ?? "open");
|
|
104
|
+
// Filters live in the URL (`t_priority=1`, `t_mine=1`), so a filtered view survives reload, Back and sharing.
|
|
105
|
+
const urlFilters = useUrlFilters(FILTER_KEYS, { prefix: "t_" });
|
|
106
|
+
const { draft, filters } = urlFilters;
|
|
107
|
+
const search = draft.search;
|
|
108
|
+
const priority = draft.priority;
|
|
109
|
+
const taskType = draft.task_type;
|
|
110
|
+
const entityType = props.defaultRecord?.entity_type ?? draft.entity_type;
|
|
111
|
+
const q = filters.search;
|
|
112
|
+
// "My tasks" needs the signed-in person's user id: the app passes it as `currentUserId` (the pickers block's
|
|
113
|
+
// useCurrentMember gives it), so this screen holds no team-members scope of its own.
|
|
114
|
+
const assigneeId = draft.mine && props.currentUserId ? props.currentUserId : filters.assignee_id || undefined;
|
|
115
|
+
const [sheet, setSheet] = useState<"new" | "edit" | null>(null);
|
|
116
|
+
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
117
|
+
const [note, setNote] = useState<string | null>(null);
|
|
118
|
+
|
|
119
|
+
const scope = { status: tab === "all" ? undefined : tab, search: q || undefined, priority: filters.priority || undefined, task_type: filters.task_type || undefined, entity_type: (props.defaultRecord?.entity_type ?? filters.entity_type) || undefined, assignee_id: assigneeId, created_by: filters.created_by || undefined, source_meeting_id: filters.source_meeting_id || undefined, entity_id: props.defaultRecord?.entity_id };
|
|
120
|
+
const list = usePageQuery<WorkspaceTask>(
|
|
121
|
+
(page, signal) => g8Get<WorkspaceTask[]>("/tasks", { limit: pageSize, offset: (page - 1) * pageSize, ...scope }, { signal }),
|
|
122
|
+
[scope.status, scope.search, scope.priority, scope.task_type, scope.entity_type, scope.entity_id, scope.assignee_id, scope.created_by, scope.source_meeting_id],
|
|
123
|
+
pageSize,
|
|
124
|
+
["tasks", "list", "screen"],
|
|
125
|
+
urlFilters.ready,
|
|
126
|
+
);
|
|
127
|
+
const recordScope = { entity_type: props.defaultRecord?.entity_type, entity_id: props.defaultRecord?.entity_id };
|
|
128
|
+
const openCount = useG8Query((signal) => g8Get<WorkspaceTask[]>("/tasks", { status: "open", limit: 1, ...recordScope }, { signal }), [recordScope.entity_type, recordScope.entity_id], { key: ["tasks", "count", "open", recordScope] });
|
|
129
|
+
const doneCount = useG8Query((signal) => g8Get<WorkspaceTask[]>("/tasks", { status: "completed", limit: 1, ...recordScope }, { signal }), [recordScope.entity_type, recordScope.entity_id], { key: ["tasks", "count", "completed", recordScope] });
|
|
130
|
+
const selection = useRowSelection<WorkspaceTask>(JSON.stringify(scope));
|
|
131
|
+
const reload = () => { void list.refetch(); void openCount.refetch(); void doneCount.refetch(); };
|
|
132
|
+
const hasFilters = Boolean(search || priority || taskType || draft.entity_type || draft.assignee_id || draft.created_by || draft.source_meeting_id || draft.mine);
|
|
133
|
+
const overdue = list.rows.filter(isOverdue).length;
|
|
134
|
+
|
|
135
|
+
const toggle = useG8Mutation((t: WorkspaceTask) => g8Patch(`/tasks/${t.id}`, { status: t.status === "completed" ? "open" : "completed" }), { invalidates: ["tasks"], onSuccess: reload });
|
|
136
|
+
const completeMany = useG8Mutation(async (ids: string[]) => {
|
|
137
|
+
const results = await Promise.allSettled(ids.map((id) => g8Patch(`/tasks/${id}`, { status: "completed" })));
|
|
138
|
+
return { done: ids.filter((_, i) => results[i].status === "fulfilled"), total: ids.length };
|
|
139
|
+
}, { invalidates: ["tasks"], onSuccess: ({ done, total }) => { selection.drop(done); setNote(done.length < total ? `Completed ${done.length} of ${total}. ${total - done.length} could not be updated; they are still selected.` : null); reload(); } });
|
|
140
|
+
const bulk = useBulkDelete<string>("/tasks", (gone, total) => {
|
|
141
|
+
selection.drop(gone);
|
|
142
|
+
setConfirmDelete(false);
|
|
143
|
+
setNote(gone.length < total ? `Deleted ${gone.length} of ${total}. ${total - gone.length} could not be deleted; they are still selected.` : null);
|
|
144
|
+
if (openId && gone.includes(openId)) setIdParam(null);
|
|
145
|
+
reload();
|
|
146
|
+
});
|
|
147
|
+
useEffect(() => { setNote(null); }, [tab, filters]);
|
|
148
|
+
const allOnPage = list.rows.length > 0 && list.rows.every((r) => selection.selected.has(r.id));
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<ScreenLayout
|
|
152
|
+
className={props.className}
|
|
153
|
+
title="Tasks"
|
|
154
|
+
description="Follow-ups across every contact and company, so nothing slips."
|
|
155
|
+
icon={<CheckSquare className="size-5" aria-hidden />}
|
|
156
|
+
actions={<>{props.toolbarActions}<Button size="sm" onClick={() => setSheet("new")}><Plus className="size-4" aria-hidden />New task</Button></>}
|
|
157
|
+
tabs={[
|
|
158
|
+
{ id: "open", label: "Open", count: totalOf(openCount.data) },
|
|
159
|
+
{ id: "completed", label: "Completed", count: totalOf(doneCount.data) },
|
|
160
|
+
{ id: "all", label: "All" },
|
|
161
|
+
]}
|
|
162
|
+
activeTab={tab}
|
|
163
|
+
onTabChange={(id) => setTab(id as Tab)}
|
|
164
|
+
stats={[
|
|
165
|
+
{ label: "Open", value: totalOf(openCount.data) ?? "-" },
|
|
166
|
+
{ label: "Completed", value: totalOf(doneCount.data) ?? "-" },
|
|
167
|
+
{ label: "Overdue on this page", value: overdue, tone: overdue ? "negative" : "neutral", hint: "Open tasks past their due date" },
|
|
168
|
+
]}
|
|
169
|
+
>
|
|
170
|
+
<SplitView
|
|
171
|
+
detailOpen={openId != null}
|
|
172
|
+
expanded={full}
|
|
173
|
+
onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
174
|
+
onCloseDetail={() => setIdParam(null)}
|
|
175
|
+
detailWidth="md"
|
|
176
|
+
list={
|
|
177
|
+
<div className="flex flex-col gap-3">
|
|
178
|
+
<Toolbar
|
|
179
|
+
search={{ value: search, onChange: (v) => urlFilters.set("search", v), debounceMs: 0, placeholder: "Search tasks by title" }}
|
|
180
|
+
filters={
|
|
181
|
+
<>
|
|
182
|
+
<select aria-label="Filter by priority" className="h-9 rounded-md border bg-background px-2 text-sm" value={priority} onChange={(e) => urlFilters.set("priority", e.target.value)}>
|
|
183
|
+
<option value="">Any priority</option><option value="1">High</option><option value="2">Medium</option><option value="3">Low</option>
|
|
184
|
+
</select>
|
|
185
|
+
<select aria-label="Filter by type" className="h-9 rounded-md border bg-background px-2 text-sm" value={taskType} onChange={(e) => urlFilters.set("task_type", e.target.value)}>
|
|
186
|
+
<option value="">Any type</option>{TASK_TYPES.map((t) => <option key={t} value={t}>{label(t)}</option>)}
|
|
187
|
+
</select>
|
|
188
|
+
{props.defaultRecord ? null : (
|
|
189
|
+
<select aria-label="Filter by linked record" className="h-9 rounded-md border bg-background px-2 text-sm" value={entityType} onChange={(e) => urlFilters.set("entity_type", e.target.value)}>
|
|
190
|
+
<option value="">Any record</option>{ENTITY_TYPES.map((t) => <option key={t} value={t}>{label(t)}</option>)}
|
|
191
|
+
</select>
|
|
192
|
+
)}
|
|
193
|
+
{props.currentUserId ? <Button variant={draft.mine ? "default" : "outline"} size="sm" aria-pressed={Boolean(draft.mine)} onClick={() => urlFilters.set("mine", draft.mine ? "" : "1")}>My tasks</Button> : null}
|
|
194
|
+
<MoreFilters activeCount={["assignee_id", "created_by", "source_meeting_id"].filter((k) => draft[k as FilterKey]).length}>
|
|
195
|
+
<TextFilter label="assignee ID" placeholder="Assignee user ID" value={draft.assignee_id} onChange={(v) => urlFilters.set("assignee_id", v)} className="w-48" />
|
|
196
|
+
<TextFilter label="creator ID" placeholder="Created by user ID" value={draft.created_by} onChange={(v) => urlFilters.set("created_by", v)} className="w-48" />
|
|
197
|
+
<TextFilter label="meeting ID" placeholder="Source meeting ID" value={draft.source_meeting_id} onChange={(v) => urlFilters.set("source_meeting_id", v)} className="w-48" />
|
|
198
|
+
</MoreFilters>
|
|
199
|
+
{hasFilters ? <Button variant="ghost" size="sm" onClick={urlFilters.clear}>Clear filters</Button> : null}
|
|
200
|
+
</>
|
|
201
|
+
}
|
|
202
|
+
/>
|
|
203
|
+
{selection.ids.length > 0 ? (
|
|
204
|
+
<div className="flex flex-wrap items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm" role="status">
|
|
205
|
+
<span className="tabular-nums">{selection.ids.length} selected</span>
|
|
206
|
+
<Button variant="outline" size="sm" loading={completeMany.loading} onClick={() => void completeMany.mutate(selection.ids)}><CheckCheck className="size-4" aria-hidden />Complete {selection.ids.length}</Button>
|
|
207
|
+
<Button variant="destructive" size="sm" onClick={() => setConfirmDelete(true)}><Trash2 className="size-4" aria-hidden />Delete {selection.ids.length}</Button>
|
|
208
|
+
<Button variant="ghost" size="sm" onClick={selection.clear}>Clear selection</Button>
|
|
209
|
+
</div>
|
|
210
|
+
) : null}
|
|
211
|
+
{list.error ? <ApiError error={list.error} title="Could not load tasks" onRetry={() => void list.refetch()} /> : null}
|
|
212
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not update the task" /> : null}
|
|
213
|
+
{note ? <p role="alert" className="text-sm text-destructive">{note}</p> : null}
|
|
214
|
+
|
|
215
|
+
<div className="overflow-hidden rounded-md border">
|
|
216
|
+
<Table>
|
|
217
|
+
<TableHeader>
|
|
218
|
+
<TableRow>
|
|
219
|
+
<TableHead className="w-8"><input type="checkbox" aria-label="Select page" checked={allOnPage} onChange={() => selection.togglePage(list.rows)} /></TableHead>
|
|
220
|
+
<TableHead className="w-8"><span className="sr-only">Done</span></TableHead>
|
|
221
|
+
<TableHead>Task</TableHead>
|
|
222
|
+
<TableHead>Linked to</TableHead>
|
|
223
|
+
<TableHead>Due</TableHead>
|
|
224
|
+
<TableHead>Priority</TableHead>
|
|
225
|
+
<TableHead>Assignee</TableHead>
|
|
226
|
+
{props.renderRowActions ? <TableHead className="w-8"><span className="sr-only">Actions</span></TableHead> : null}
|
|
227
|
+
</TableRow>
|
|
228
|
+
</TableHeader>
|
|
229
|
+
<TableBody>
|
|
230
|
+
{list.loading && list.rows.length === 0
|
|
231
|
+
? Array.from({ length: 6 }, (_, i) => <TableRow key={`sk${i}`}><TableCell colSpan={7 + (props.renderRowActions ? 1 : 0)}><Skeleton className="h-6 w-full" /></TableCell></TableRow>)
|
|
232
|
+
: list.rows.map((t) => {
|
|
233
|
+
const done = t.status === "completed";
|
|
234
|
+
return (
|
|
235
|
+
<TableRow key={t.id} data-testid={`task-row-${t.id}`} data-state={t.id === openId ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => setIdParam(t.id))}>
|
|
236
|
+
<TableCell onClick={(e) => e.stopPropagation()}><input type="checkbox" aria-label={`Select ${t.title}`} checked={selection.selected.has(t.id)} onChange={() => selection.toggle(t)} /></TableCell>
|
|
237
|
+
<TableCell onClick={(e) => e.stopPropagation()}><input type="checkbox" aria-label={`${done ? "Reopen" : "Complete"} ${t.title}`} checked={done} onChange={() => void toggle.mutate(t)} /></TableCell>
|
|
238
|
+
<TableCell className={done ? "text-muted-foreground line-through" : "font-medium"}><RecordLink param="id" id={t.id} onOpen={() => setIdParam(t.id)}>{t.title}</RecordLink></TableCell>
|
|
239
|
+
<TableCell>{t.entity_label ? <span>{t.entity_label}{t.entity_type ? <span className="ml-1 text-xs text-muted-foreground">{label(t.entity_type)}</span> : null}</span> : <span className="text-muted-foreground">-</span>}</TableCell>
|
|
240
|
+
<TableCell className="whitespace-nowrap">{t.due_date ? <span className={isOverdue(t) ? "text-destructive" : undefined}>{formatDay(t.due_date)}{isOverdue(t) ? " (overdue)" : ""}</span> : <span className="text-muted-foreground">-</span>}</TableCell>
|
|
241
|
+
<TableCell><PriorityPill priority={t.priority} /></TableCell>
|
|
242
|
+
<TableCell>{t.assignee_name || <span className="text-muted-foreground">-</span>}</TableCell>
|
|
243
|
+
{props.renderRowActions ? <TableCell onClick={(e) => e.stopPropagation()}>{props.renderRowActions(t)}</TableCell> : null}
|
|
244
|
+
</TableRow>
|
|
245
|
+
);
|
|
246
|
+
})}
|
|
247
|
+
</TableBody>
|
|
248
|
+
</Table>
|
|
249
|
+
{!list.loading && !list.error && list.rows.length === 0 ? (
|
|
250
|
+
<EmptyState
|
|
251
|
+
title={hasFilters ? "No tasks match these filters" : tab === "completed" ? "Nothing completed yet" : "No tasks yet"}
|
|
252
|
+
description={hasFilters ? "Try fewer filters." : "Add a follow-up so nothing slips."}
|
|
253
|
+
action={hasFilters ? undefined : <Button onClick={() => setSheet("new")}>New task</Button>}
|
|
254
|
+
/>
|
|
255
|
+
) : null}
|
|
256
|
+
<Pager page={list.page} pageCount={list.pageCount} total={list.total} noun="tasks" hasNext={list.hasNext} loading={list.loading} onPage={list.setPage} />
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
}
|
|
260
|
+
detail={openId ? (
|
|
261
|
+
<TaskPanel key={openId} full={full} setFull={(v) => setView(v ? "full" : null)} taskId={openId} onClose={() => setIdParam(null)} onChanged={reload} onDeleted={() => { setIdParam(null); reload(); }}
|
|
262
|
+
onEdit={() => setSheet("edit")} sheetEditing={sheet === "edit"} onCloseEdit={() => setSheet(null)}
|
|
263
|
+
onOpenRecord={props.onOpenRecord} renderActions={props.renderDetailActions} />
|
|
264
|
+
) : null}
|
|
265
|
+
/>
|
|
266
|
+
|
|
267
|
+
<FormSheet open={sheet === "new"} onOpenChange={(o) => setSheet(o ? "new" : null)} title="New task" description={props.defaultRecord ? `Linked to this ${props.defaultRecord.entity_type}.` : "A standalone follow-up."}>
|
|
268
|
+
<TaskForm record={props.defaultRecord} onCancel={() => setSheet(null)} onSaved={(t) => { setSheet(null); reload(); if (t?.id) setIdParam(t.id); }} />
|
|
269
|
+
</FormSheet>
|
|
270
|
+
|
|
271
|
+
<ConfirmSpendDialog open={confirmDelete} tier="destructive" title={`Delete ${plural(selection.ids.length, "task")}`} description="They are removed for everyone." affected={selection.ids.length}
|
|
272
|
+
confirming={bulk.loading} onConfirm={() => void bulk.mutate(selection.ids)} onCancel={() => setConfirmDelete(false)} />
|
|
273
|
+
</ScreenLayout>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface TaskPanelProps {
|
|
278
|
+
full: boolean;
|
|
279
|
+
setFull: (full: boolean) => void;
|
|
280
|
+
taskId: string;
|
|
281
|
+
onClose: () => void;
|
|
282
|
+
onChanged: () => void;
|
|
283
|
+
onDeleted: () => void;
|
|
284
|
+
onEdit: () => void;
|
|
285
|
+
sheetEditing: boolean;
|
|
286
|
+
onCloseEdit: () => void;
|
|
287
|
+
onOpenRecord?: TasksScreenProps["onOpenRecord"];
|
|
288
|
+
renderActions?: TasksScreenProps["renderDetailActions"];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function TaskPanel({ full, setFull, taskId, onClose, onChanged, onDeleted, onEdit, sheetEditing, onCloseEdit, onOpenRecord, renderActions }: TaskPanelProps) {
|
|
292
|
+
const [confirm, setConfirm] = useState(false);
|
|
293
|
+
const query = useG8Query((signal) => g8Get<WorkspaceTask>(`/tasks/${taskId}`, {}, { signal }), [taskId], { key: ["tasks", "detail", taskId] });
|
|
294
|
+
const toggle = useG8Mutation((t: WorkspaceTask) => g8Patch(`/tasks/${t.id}`, { status: t.status === "completed" ? "open" : "completed" }), { invalidates: ["tasks"], onSuccess: () => { void query.refetch(); onChanged(); } });
|
|
295
|
+
const remove = useG8Mutation(async () => {
|
|
296
|
+
try { await g8Delete(`/tasks/${taskId}`); } catch (e) { if (errorStatus(e) !== 404) throw e; }
|
|
297
|
+
}, { invalidates: [["tasks", "list"], ["tasks", "count"], ["tasks", "record"]], onSuccess: () => { setConfirm(false); onDeleted(); } });
|
|
298
|
+
|
|
299
|
+
if (query.loading && !query.data) return <div className="flex flex-col gap-3 p-4"><Skeleton className="h-14 w-full" /><Skeleton className="h-32 w-full" /></div>;
|
|
300
|
+
if (query.error && !query.data) return <div className="p-4"><ApiError error={query.error} title="Could not load this task" onRetry={() => void query.refetch()} /></div>;
|
|
301
|
+
const task = query.data!.data;
|
|
302
|
+
const done = task.status === "completed";
|
|
303
|
+
const dash = <span className="text-muted-foreground">-</span>;
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<>
|
|
307
|
+
<DetailPanel
|
|
308
|
+
title={task.title}
|
|
309
|
+
subtitle={task.entity_label ? `${task.entity_type ? label(task.entity_type) + ": " : ""}${task.entity_label}` : "Standalone task"}
|
|
310
|
+
badges={<StatusPill tone={done ? "positive" : isOverdue(task) ? "negative" : "info"} dot>{done ? "Completed" : isOverdue(task) ? "Overdue" : "Open"}</StatusPill>}
|
|
311
|
+
onClose={onClose}
|
|
312
|
+
onExpand={full ? undefined : () => setFull(true)}
|
|
313
|
+
onCollapse={full ? () => setFull(false) : undefined}
|
|
314
|
+
actions={
|
|
315
|
+
<>
|
|
316
|
+
{renderActions?.(task)}
|
|
317
|
+
{task.entity_type && task.entity_id && onOpenRecord ? <Button variant="outline" size="sm" onClick={() => onOpenRecord({ entity_type: task.entity_type!, entity_id: task.entity_id! })}>Open {task.entity_type}</Button> : null}
|
|
318
|
+
<Button variant="outline" size="sm" loading={toggle.loading} onClick={() => void toggle.mutate(task)}>{done ? "Reopen" : "Mark complete"}</Button>
|
|
319
|
+
<Button variant="outline" size="sm" onClick={onEdit}>Edit</Button>
|
|
320
|
+
<Button variant="destructive" size="sm" onClick={() => setConfirm(true)}>Delete</Button>
|
|
321
|
+
</>
|
|
322
|
+
}
|
|
323
|
+
>
|
|
324
|
+
<div className="flex flex-col gap-4 p-4">
|
|
325
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not update the task" /> : null}
|
|
326
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the task" /> : null}
|
|
327
|
+
<KeyValueList columns={2} items={[
|
|
328
|
+
{ label: "Due", value: task.due_date ? formatDay(task.due_date) : dash },
|
|
329
|
+
{ label: "Priority", value: <PriorityPill priority={task.priority} /> },
|
|
330
|
+
{ label: "Type", value: task.task_type ? label(task.task_type) : dash },
|
|
331
|
+
{ label: "Assignee", value: task.assignee_name || dash },
|
|
332
|
+
{ label: "Created", value: task.created_at ? formatDay(task.created_at) : dash },
|
|
333
|
+
...(task.description ? [{ label: "Description", value: task.description }] : []),
|
|
334
|
+
]} />
|
|
335
|
+
</div>
|
|
336
|
+
</DetailPanel>
|
|
337
|
+
<FormSheet open={sheetEditing} onOpenChange={(o) => { if (!o) onCloseEdit(); }} title="Edit task">
|
|
338
|
+
<TaskForm task={task} onCancel={onCloseEdit} onSaved={() => { onCloseEdit(); void query.refetch(); onChanged(); }} />
|
|
339
|
+
</FormSheet>
|
|
340
|
+
<ConfirmSpendDialog open={confirm} tier="destructive" title="Delete this task" description={task.title} affected={1}
|
|
341
|
+
confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setConfirm(false)} />
|
|
342
|
+
</>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
interface TaskFormProps {
|
|
347
|
+
task?: WorkspaceTask;
|
|
348
|
+
record?: TasksScreenProps["defaultRecord"];
|
|
349
|
+
onSaved: (task?: WorkspaceTask) => void;
|
|
350
|
+
onCancel: () => void;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function TaskForm({ task, record, onSaved, onCancel }: TaskFormProps) {
|
|
354
|
+
const [title, setTitle] = useState(task?.title ?? "");
|
|
355
|
+
const [description, setDescription] = useState(task?.description ?? "");
|
|
356
|
+
const [due, setDue] = useState(task?.due_date ? task.due_date.slice(0, 10) : "");
|
|
357
|
+
const [priority, setPriority] = useState(String(task ? task.priority ?? 0 : 2));
|
|
358
|
+
const [taskType, setTaskType] = useState(task?.task_type ?? "");
|
|
359
|
+
const [error, setError] = useState<string | null>(null);
|
|
360
|
+
const save = useG8Mutation(async () => {
|
|
361
|
+
const body = { title: title.trim(), description: description.trim() || null, due_date: due || null, priority: Number(priority), task_type: taskType || null };
|
|
362
|
+
if (task) {
|
|
363
|
+
// An untouched priority is not sent, so editing a task without one never invents a priority.
|
|
364
|
+
const { priority: _skip, ...rest } = body;
|
|
365
|
+
const patch = Number(priority) === (task.priority ?? 0) ? rest : body;
|
|
366
|
+
return (await g8Patch<WorkspaceTask>(`/tasks/${task.id}`, patch)).data;
|
|
367
|
+
}
|
|
368
|
+
return (await g8Post<WorkspaceTask>("/tasks", { ...body, ...(record ? { entity_type: record.entity_type, entity_id: record.entity_id } : {}) })).data;
|
|
369
|
+
}, { invalidates: ["tasks"], onSuccess: (t) => onSaved(t) });
|
|
370
|
+
const submit = (e: FormEvent) => {
|
|
371
|
+
e.preventDefault();
|
|
372
|
+
if (!title.trim()) return setError("Give the task a title.");
|
|
373
|
+
setError(null);
|
|
374
|
+
void save.mutate(undefined);
|
|
375
|
+
};
|
|
376
|
+
return (
|
|
377
|
+
<form noValidate onSubmit={submit} className="flex flex-col gap-3" aria-label={task ? "Edit task" : "New task"}>
|
|
378
|
+
<div className="flex flex-col gap-1"><Label htmlFor="task-title">Title</Label><Input id="task-title" value={title} onChange={(e) => setTitle(e.target.value)} /></div>
|
|
379
|
+
<div className="flex flex-col gap-1"><Label htmlFor="task-desc">Description</Label><Textarea id="task-desc" rows={3} value={description} onChange={(e) => setDescription(e.target.value)} /></div>
|
|
380
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
381
|
+
<div className="flex flex-col gap-1"><Label htmlFor="task-due">Due date</Label><Input id="task-due" type="date" value={due} onChange={(e) => setDue(e.target.value)} /></div>
|
|
382
|
+
<div className="flex flex-col gap-1"><Label htmlFor="task-priority">Priority</Label>
|
|
383
|
+
<select id="task-priority" className="h-9 rounded-md border bg-background px-2 text-sm" value={priority} onChange={(e) => setPriority(e.target.value)}>
|
|
384
|
+
<option value="1">High</option><option value="2">Medium</option><option value="3">Low</option><option value="0">None</option>
|
|
385
|
+
</select></div>
|
|
386
|
+
<div className="flex flex-col gap-1"><Label htmlFor="task-type">Type</Label>
|
|
387
|
+
<select id="task-type" className="h-9 rounded-md border bg-background px-2 text-sm" value={taskType} onChange={(e) => setTaskType(e.target.value)}>
|
|
388
|
+
<option value="">None</option>{TASK_TYPES.map((t) => <option key={t} value={t}>{label(t)}</option>)}
|
|
389
|
+
</select></div>
|
|
390
|
+
</div>
|
|
391
|
+
{error ? <p role="alert" className="text-sm text-destructive">{error}</p> : null}
|
|
392
|
+
{save.error ? <ApiError error={save.error} title="Could not save the task" /> : null}
|
|
393
|
+
<div className="flex justify-end gap-2">
|
|
394
|
+
<Button type="button" variant="ghost" onClick={onCancel}>Cancel</Button>
|
|
395
|
+
<Button type="submit" loading={save.loading}>{task ? "Save changes" : "Create task"}</Button>
|
|
396
|
+
</div>
|
|
397
|
+
</form>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useAllLists: every list of the workspace, for a screen that must look one up by id (its size, its name). The lists API
|
|
5
|
+
* pages (`page`, `limit`), so this reads page after page (100 each, up to 20 pages) instead of one capped `limit=200`
|
|
6
|
+
* call; `truncated` says the page cap was reached. To CHOOSE a list use ListSelect (searchable, paged).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations: GET /lists?page&limit (lists.listLists) read - through lib/read-lists.ts
|
|
9
|
+
*/
|
|
10
|
+
import { useAllPages } from "@/g8/hooks/use-all-pages";
|
|
11
|
+
import type { ListSelectOption } from "@/g8/components/list-select";
|
|
12
|
+
import { readLists } from "../lib/read-lists";
|
|
13
|
+
|
|
14
|
+
const PAGE = 100;
|
|
15
|
+
|
|
16
|
+
export function useAllLists(deps: readonly unknown[] = [], enabled = true) {
|
|
17
|
+
return useAllPages<ListSelectOption>((page, signal) => readLists("", page, PAGE, signal), deps, { enabled });
|
|
18
|
+
}
|
|
@@ -12,7 +12,12 @@ import type { CrmEntity, FieldDef } from "../lib/crm-types";
|
|
|
12
12
|
*/
|
|
13
13
|
export function useCustomFields(entity: CrmEntity, listId?: number, enabled = true) {
|
|
14
14
|
const path = entity === "contacts" ? "/fields" : "/fields/companies";
|
|
15
|
-
const query = useG8Query((signal) => g8Get<FieldDef[]>(path, { list_id: listId }, { signal }), [path, listId], {
|
|
15
|
+
const query = useG8Query((signal) => g8Get<FieldDef[]>(path, { list_id: listId }, { signal }), [path, listId], {
|
|
16
|
+
enabled,
|
|
17
|
+
// The schema rarely changes: share it between the table, the detail and the form, fresh for 30 s.
|
|
18
|
+
key: ["fields", entity, { listId }],
|
|
19
|
+
staleTime: 30_000,
|
|
20
|
+
});
|
|
16
21
|
const all = (query.data?.data ?? []) as FieldDef[];
|
|
17
22
|
return { fields: all.filter(isCustomField), all, loading: query.loading, error: query.error, refetch: query.refetch };
|
|
18
23
|
}
|