@graph8/sdk 0.370.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +53 -11
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +165 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +57 -20
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +20 -1
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +38 -13
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-screen.tsx +33 -6
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +34 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CorrespondenceImport: bring existing correspondence in, as a batch or as one file.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. importAccountCorrespondence (POST /accounts/{account_id}/correspondence/import) write a JSON array of messages; PARTIAL SUCCESS is normal (the status is 200 even when rows failed), so the imported, skipped and error counts are shown
|
|
8
|
+
* 2. uploadAccountCorrespondence (POST /accounts/{account_id}/correspondence/upload) write one text or .eml file becomes one record (multipart)
|
|
9
|
+
* A message with an `external_id` that is already stored is SKIPPED, which makes a re-run safe; messages without one are
|
|
10
|
+
* added again on every import, and the form says how many of those the batch holds. An uploaded file is dated NOW (the API
|
|
11
|
+
* has no date for it): to date a message when it happened, record it with CorrespondenceForm instead. Files are read as text,
|
|
12
|
+
* so only text types are offered. Nothing here is billable, external or destructive.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button, Label, Textarea } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { plural } from "@/g8/lib/plural";
|
|
21
|
+
import { CORRESPONDENCE_CHANNELS, DEFAULT_MAX_BYTES, DIRECTIONS, checkFiles, fileLimit, fmtSize, parseImportItems } from "../lib/documents-types";
|
|
22
|
+
import type { ImportResult, Rec } from "../lib/documents-types";
|
|
23
|
+
|
|
24
|
+
export interface CorrespondenceImportProps {
|
|
25
|
+
accountId: number;
|
|
26
|
+
maxBytes?: number;
|
|
27
|
+
onImported?: () => void;
|
|
28
|
+
onDone?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TEXT_TYPES = ["eml", "txt", "md", "csv", "json"] as const;
|
|
32
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
33
|
+
const EXAMPLE = '[{"channel":"email","direction":"inbound","subject":"Re: pricing","content":"Thanks for the call...","correspondence_date":"2026-09-30T14:00:00Z","external_id":"msg-1001"}]';
|
|
34
|
+
|
|
35
|
+
export function CorrespondenceImport({ accountId, maxBytes = DEFAULT_MAX_BYTES, onImported, onDone }: CorrespondenceImportProps) {
|
|
36
|
+
const [mode, setMode] = useState<"batch" | "file">("batch");
|
|
37
|
+
const [text, setText] = useState("");
|
|
38
|
+
const [file, setFile] = useState<File | null>(null);
|
|
39
|
+
const [channel, setChannel] = useState("");
|
|
40
|
+
const [direction, setDirection] = useState("");
|
|
41
|
+
const [result, setResult] = useState<ImportResult | null>(null);
|
|
42
|
+
const [saved, setSaved] = useState(false);
|
|
43
|
+
const base = `/accounts/${encodeURIComponent(String(accountId))}/correspondence`;
|
|
44
|
+
const parsed = text.trim() ? parseImportItems(text) : null;
|
|
45
|
+
const noId = parsed && !parsed.problem ? parsed.items.filter((i) => !i.external_id).length : 0;
|
|
46
|
+
const fileCheck = file ? checkFiles([file], TEXT_TYPES, fileLimit(maxBytes)) : null;
|
|
47
|
+
|
|
48
|
+
const importBatch = useG8Mutation(async (items: Rec[]): Promise<ImportResult> => (await g8Post<ImportResult>(`${base}/import`, { items })).data ?? {}, {
|
|
49
|
+
onSuccess: (r) => { setResult(r); onImported?.(); },
|
|
50
|
+
invalidates: [["documents", "correspondence"]],
|
|
51
|
+
});
|
|
52
|
+
const uploadOne = useG8Mutation(
|
|
53
|
+
async (): Promise<Rec> => {
|
|
54
|
+
const form = new FormData();
|
|
55
|
+
form.append("file", file!);
|
|
56
|
+
form.append("channel", channel);
|
|
57
|
+
form.append("direction", direction);
|
|
58
|
+
return (await g8Post<Rec>(`${base}/upload`, form)).data;
|
|
59
|
+
},
|
|
60
|
+
{ onSuccess: () => { setSaved(true); setFile(null); onImported?.(); }, invalidates: [["documents", "correspondence"]] },
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const err = importBatch.error ?? uploadOne.error;
|
|
64
|
+
return (
|
|
65
|
+
<div className="grid gap-3">
|
|
66
|
+
<div role="group" aria-label="How to bring messages in" className="flex gap-1">
|
|
67
|
+
<Button size="sm" variant={mode === "batch" ? "default" : "outline"} aria-pressed={mode === "batch"} onClick={() => setMode("batch")}>Paste a batch</Button>
|
|
68
|
+
<Button size="sm" variant={mode === "file" ? "default" : "outline"} aria-pressed={mode === "file"} onClick={() => setMode("file")}>Upload a file</Button>
|
|
69
|
+
</div>
|
|
70
|
+
{mode === "batch" ? (
|
|
71
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (parsed && !parsed.problem) void importBatch.mutate(parsed.items); }}>
|
|
72
|
+
<div>
|
|
73
|
+
<Label htmlFor="g8-ci-json" className="mb-1">Messages as JSON</Label>
|
|
74
|
+
<Textarea id="g8-ci-json" rows={10} value={text} onChange={(e) => { setText(e.target.value); setResult(null); }} placeholder={EXAMPLE} className="font-mono text-xs" />
|
|
75
|
+
<p className="text-muted-foreground mt-1 text-xs">Each message needs channel (email, linkedin or call_notes), direction (inbound or outbound), content and correspondence_date. Add external_id to make a re-run skip what is already there.</p>
|
|
76
|
+
</div>
|
|
77
|
+
{parsed?.problem ? <p role="alert" className="text-error text-xs">{parsed.problem}</p> : null}
|
|
78
|
+
{parsed && !parsed.problem ? <p className="text-xs">{plural(parsed.items.length, "message")} ready.{noId > 0 ? ` ${plural(noId, "message")} ${noId === 1 ? "has" : "have"} no external_id and would be added again if you import this twice.` : ""}</p> : null}
|
|
79
|
+
{result ? (
|
|
80
|
+
<div role="status" className="grid gap-1 rounded-md border p-3 text-sm">
|
|
81
|
+
<p className="font-medium">{result.imported ?? 0} imported, {result.skipped ?? 0} skipped{(result.errors ?? []).length > 0 ? `, ${(result.errors ?? []).length} failed` : ""}.</p>
|
|
82
|
+
{(result.errors ?? []).map((m) => <p key={m} className="text-error text-xs">{m}</p>)}
|
|
83
|
+
</div>
|
|
84
|
+
) : null}
|
|
85
|
+
<div className="flex gap-2"><Button type="submit" loading={importBatch.loading} disabled={!parsed || Boolean(parsed.problem)}>Import messages</Button>{onDone ? <Button type="button" variant="outline" onClick={onDone}>Close</Button> : null}</div>
|
|
86
|
+
</form>
|
|
87
|
+
) : (
|
|
88
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (file && fileCheck?.ok.length === 1 && channel && direction) void uploadOne.mutate(undefined); }}>
|
|
89
|
+
<div>
|
|
90
|
+
<Label htmlFor="g8-ci-file" className="mb-1">File</Label>
|
|
91
|
+
<input id="g8-ci-file" type="file" accept={TEXT_TYPES.map((x) => `.${x}`).join(",")} className="border-input bg-background w-full rounded-md border p-2 text-sm" onChange={(e) => { setFile(e.target.files?.[0] ?? null); setSaved(false); }} />
|
|
92
|
+
<p className="text-muted-foreground mt-1 text-xs">A forwarded .eml or an exported thread, as text ({TEXT_TYPES.join(", ")}), up to {fmtSize(fileLimit(maxBytes))}. It is recorded with today's date; to date it when it happened, record the message instead.</p>
|
|
93
|
+
</div>
|
|
94
|
+
{fileCheck?.problems.length ? <ul role="alert" className="text-error list-disc pl-5 text-xs">{fileCheck.problems.map((p) => <li key={p}>{p}</li>)}</ul> : null}
|
|
95
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
96
|
+
<div><Label htmlFor="g8-ci-channel" className="mb-1">Channel</Label><select id="g8-ci-channel" className={SELECT} value={channel} onChange={(e) => setChannel(e.target.value)}><option value="">Choose a channel</option>{CORRESPONDENCE_CHANNELS.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}</select></div>
|
|
97
|
+
<div><Label htmlFor="g8-ci-direction" className="mb-1">Direction</Label><select id="g8-ci-direction" className={SELECT} value={direction} onChange={(e) => setDirection(e.target.value)}><option value="">Choose a direction</option>{DIRECTIONS.map((d) => <option key={d.value} value={d.value}>{d.label}</option>)}</select></div>
|
|
98
|
+
</div>
|
|
99
|
+
{saved ? <p role="status" className="text-sm">The file was recorded.</p> : null}
|
|
100
|
+
<div className="flex gap-2"><Button type="submit" loading={uploadOne.loading} disabled={!file || fileCheck?.ok.length !== 1 || !channel || !direction}>Record file</Button>{onDone ? <Button type="button" variant="outline" onClick={onDone}>Close</Button> : null}</div>
|
|
101
|
+
</form>
|
|
102
|
+
)}
|
|
103
|
+
{err ? <ApiError error={err} title="Could not bring the messages in" /> : null}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CorrespondencePanel: what has been said to one account, across channels, as a finished page: the list with a channel and
|
|
5
|
+
* person filter, the selected message, record one, bring a batch in, delete.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listAccountCorrespondence (GET /accounts/{account_id}/correspondence) read newest first, paged 50 at a time with Load more; filtered by channel and person on the server
|
|
9
|
+
* 2. deleteAccountCorrespondence (DELETE .../correspondence/{correspondence_id}) DESTRUCTIVE removes the record; documents already generated from it are NOT rewritten; behind ConfirmSpendDialog
|
|
10
|
+
* Recording one message (CorrespondenceForm) and importing (CorrespondenceImport) are the composed parts. Pass `accountId`
|
|
11
|
+
* (a company id); without one the panel asks for an account.
|
|
12
|
+
* URL: `?cid=` the open message, `&view=full` as a page.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import type { ReactNode } from "react";
|
|
17
|
+
import { Mail, MessagesSquare, Plus, Trash2, Upload } from "lucide-react";
|
|
18
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
19
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
20
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
21
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
22
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
23
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
24
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
25
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
26
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
27
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
28
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
29
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
30
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
31
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
32
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
33
|
+
import { byOffset } from "@/g8/lib/paging";
|
|
34
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
35
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
36
|
+
import { ContactPicker } from "@/g8/pickers/components/contact-picker";
|
|
37
|
+
import type { ContactOption } from "@/g8/pickers/components/contact-picker";
|
|
38
|
+
import { CORRESPONDENCE_CHANNELS, DEFAULT_MAX_BYTES, rowsOf, totalOf, when } from "../lib/documents-types";
|
|
39
|
+
import type { Correspondence, Rec } from "../lib/documents-types";
|
|
40
|
+
import { CorrespondenceForm } from "./correspondence-form";
|
|
41
|
+
import { CorrespondenceImport } from "./correspondence-import";
|
|
42
|
+
|
|
43
|
+
export interface CorrespondencePanelProps {
|
|
44
|
+
accountId: number | null;
|
|
45
|
+
renderRowActions?: (c: Correspondence) => ReactNode;
|
|
46
|
+
toolbarActions?: ReactNode;
|
|
47
|
+
maxBytes?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PAGE = 50;
|
|
51
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
52
|
+
const label = (list: readonly { value: string; label: string }[], v: string | null | undefined): string => list.find((x) => x.value === v)?.label ?? (v ? v.replace(/_/g, " ") : "");
|
|
53
|
+
const headline = (c: Correspondence): string => c.subject || (c.content ? `${c.content.slice(0, 80)}${c.content.length > 80 ? "..." : ""}` : "No subject");
|
|
54
|
+
|
|
55
|
+
export function CorrespondencePanel({ accountId, renderRowActions, toolbarActions, maxBytes = DEFAULT_MAX_BYTES }: CorrespondencePanelProps) {
|
|
56
|
+
const brand = useBrand();
|
|
57
|
+
const [cid, setCid] = useUrlState("cid", "");
|
|
58
|
+
const [view, setView] = useUrlState("view", "");
|
|
59
|
+
const [search, setSearch] = useState("");
|
|
60
|
+
const [channel, setChannel] = useState("");
|
|
61
|
+
const [person, setPerson] = useState<ContactOption | null>(null);
|
|
62
|
+
const [sheet, setSheet] = useState<null | "record" | "import">(null);
|
|
63
|
+
const [toDelete, setToDelete] = useState<Correspondence | null>(null);
|
|
64
|
+
|
|
65
|
+
const list = useLoadMoreList<Correspondence>(
|
|
66
|
+
byOffset(PAGE, async (offset, signal) => {
|
|
67
|
+
if (accountId == null) return { rows: [], hasNext: false };
|
|
68
|
+
const env = await g8Get<Rec>(`/accounts/${encodeURIComponent(String(accountId))}/correspondence`, { channel: channel || undefined, stakeholder_id: person ? String(person.id) : undefined, limit: PAGE, offset }, { signal });
|
|
69
|
+
const rows = rowsOf<Correspondence>(env.data);
|
|
70
|
+
const total = totalOf(env.data);
|
|
71
|
+
return { rows, hasNext: total != null ? offset + rows.length < total : rows.length >= PAGE };
|
|
72
|
+
}),
|
|
73
|
+
[accountId, channel, person?.id],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const remove = useG8Mutation(
|
|
77
|
+
(c: Correspondence) => g8Delete(`/accounts/${encodeURIComponent(String(accountId))}/correspondence/${encodeURIComponent(c.id)}`),
|
|
78
|
+
{ onSuccess: (_r, c) => { setToDelete(null); if (c.id === cid) { setCid(null); setView(null); } void list.refetch(); }, invalidates: [["documents", "correspondence"]] },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const rows = list.data ?? [];
|
|
82
|
+
const q = search.trim().toLowerCase();
|
|
83
|
+
const shown = rows.filter((r) => !q || `${r.subject ?? ""} ${r.content ?? ""}`.toLowerCase().includes(q));
|
|
84
|
+
const selected = rows.find((r) => r.id === cid) ?? null;
|
|
85
|
+
const closeDetail = (): void => { setCid(null); setView(null); };
|
|
86
|
+
const n = (f: (r: Correspondence) => boolean): string => (list.data ? `${rows.filter(f).length.toLocaleString()}${list.hasMore ? "+" : ""}` : "...");
|
|
87
|
+
|
|
88
|
+
if (accountId == null) {
|
|
89
|
+
return <EmptyState icon={<MessagesSquare className="size-8" />} title="Choose an account" description="Pick a company above to see what has been said to it, or to record a message." />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const body = list.error ? <ApiError error={list.error} title="Could not load correspondence" onRetry={() => void list.refetch()} /> : list.loading && !list.data ? (
|
|
93
|
+
<div aria-busy="true" className="grid gap-2"><Skeleton className="h-14 w-full" /><Skeleton className="h-14 w-full" /></div>
|
|
94
|
+
) : shown.length === 0 ? (
|
|
95
|
+
<EmptyState icon={<Mail className="size-8" />} title={rows.length === 0 && !channel && !person ? "Nothing recorded yet" : "No messages match"} description={rows.length === 0 && !channel && !person ? `Record the emails, LinkedIn messages and call notes that happened off ${brand.name}, so every generated document knows the relationship.` : "Try another search or filter."} action={rows.length === 0 && !channel && !person ? <Button onClick={() => setSheet("record")}>Record a message</Button> : undefined} />
|
|
96
|
+
) : (
|
|
97
|
+
<>
|
|
98
|
+
<ul className="grid gap-2">
|
|
99
|
+
{shown.map((r) => (
|
|
100
|
+
<li key={r.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${r.id === cid ? "bg-muted" : ""}`}>
|
|
101
|
+
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setCid(r.id)}>
|
|
102
|
+
<span className="block truncate font-medium">{headline(r)}</span>
|
|
103
|
+
<span className="text-muted-foreground block truncate text-xs">{label(CORRESPONDENCE_CHANNELS, r.channel) || "-"} · {when(r.correspondence_date) || "-"}</span>
|
|
104
|
+
</button>
|
|
105
|
+
{r.direction ? <StatusPill tone={r.direction === "inbound" ? "info" : "neutral"}>{label([{ value: "inbound", label: "Inbound" }, { value: "outbound", label: "Outbound" }], r.direction)}</StatusPill> : null}
|
|
106
|
+
{renderRowActions?.(r)}
|
|
107
|
+
</li>
|
|
108
|
+
))}
|
|
109
|
+
</ul>
|
|
110
|
+
<LoadMore shown={rows.length} noun="messages" hasMore={list.hasMore} loading={list.loadingMore} onLoadMore={list.loadMore} />
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="grid gap-4">
|
|
116
|
+
<StatStrip items={[
|
|
117
|
+
{ label: "Messages", value: list.data ? `${rows.length.toLocaleString()}${list.hasMore ? "+" : ""}` : "..." },
|
|
118
|
+
{ label: "Inbound", value: n((r) => r.direction === "inbound") },
|
|
119
|
+
{ label: "Outbound", value: n((r) => r.direction === "outbound") },
|
|
120
|
+
{ label: "Latest", value: rows[0]?.correspondence_date ? new Date(rows[0].correspondence_date).toLocaleDateString() : "-", hint: "The newest message loaded." },
|
|
121
|
+
]} />
|
|
122
|
+
<SplitView
|
|
123
|
+
list={(
|
|
124
|
+
<div className="grid gap-3">
|
|
125
|
+
<Toolbar
|
|
126
|
+
search={{ value: search, onChange: setSearch, placeholder: "Search messages" }}
|
|
127
|
+
filters={(
|
|
128
|
+
<>
|
|
129
|
+
<select aria-label="Filter by channel" className={SELECT} value={channel} onChange={(e) => { setChannel(e.target.value); closeDetail(); }}><option value="">All channels</option>{CORRESPONDENCE_CHANNELS.map((c) => <option key={c.value} value={c.value}>{c.label}</option>)}</select>
|
|
130
|
+
<span className="flex items-center gap-1"><ContactPicker label="Person" value={person} onChange={(p) => { setPerson(p); closeDetail(); }} />{person ? <Button size="sm" variant="ghost" onClick={() => { setPerson(null); closeDetail(); }}>Clear person</Button> : null}</span>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
actions={<>{toolbarActions}<Button size="sm" variant="outline" onClick={() => setSheet("import")}><Upload className="size-4" />Import</Button><Button size="sm" onClick={() => setSheet("record")}><Plus className="size-4" />Record message</Button></>}
|
|
134
|
+
/>
|
|
135
|
+
{body}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
detail={selected ? (
|
|
139
|
+
<DetailPanel
|
|
140
|
+
key={selected.id}
|
|
141
|
+
title={headline(selected)}
|
|
142
|
+
subtitle={`${label(CORRESPONDENCE_CHANNELS, selected.channel)} · ${when(selected.correspondence_date)}`}
|
|
143
|
+
badges={selected.direction ? <StatusPill tone={selected.direction === "inbound" ? "info" : "neutral"}>{selected.direction === "inbound" ? "Inbound" : "Outbound"}</StatusPill> : undefined}
|
|
144
|
+
actions={<Button size="sm" variant="outline" onClick={() => setToDelete(selected)}><Trash2 className="size-4" />Delete</Button>}
|
|
145
|
+
onClose={closeDetail}
|
|
146
|
+
onExpand={() => setView("full")}
|
|
147
|
+
onCollapse={view === "full" ? () => setView(null) : undefined}
|
|
148
|
+
tabs={[
|
|
149
|
+
{ id: "message", label: "Message", content: selected.content ? <pre aria-label="Message text" className="bg-muted max-h-[28rem] overflow-auto whitespace-pre-wrap rounded-md p-3 text-sm">{selected.content}</pre> : <EmptyState compact title="No text" /> },
|
|
150
|
+
{ id: "details", label: "Details", content: (
|
|
151
|
+
<KeyValueList columns={2} items={[
|
|
152
|
+
{ label: "Channel", value: label(CORRESPONDENCE_CHANNELS, selected.channel) },
|
|
153
|
+
{ label: "Direction", value: selected.direction ? (selected.direction === "inbound" ? "Inbound" : "Outbound") : "" },
|
|
154
|
+
{ label: "Happened", value: when(selected.correspondence_date) },
|
|
155
|
+
{ label: "Recorded", value: when(selected.created_at) },
|
|
156
|
+
{ label: "Source", value: selected.source },
|
|
157
|
+
{ label: "Read by the AI", value: selected.indexed_in_rag == null ? "" : selected.indexed_in_rag ? "Yes" : "Not yet" },
|
|
158
|
+
{ label: "Person id", value: selected.stakeholder_id, mono: true },
|
|
159
|
+
{ label: "Your id", value: selected.external_id, mono: true },
|
|
160
|
+
{ label: "Record id", value: selected.id, mono: true },
|
|
161
|
+
]} />
|
|
162
|
+
) },
|
|
163
|
+
]}
|
|
164
|
+
/>
|
|
165
|
+
) : null}
|
|
166
|
+
detailOpen={Boolean(selected)}
|
|
167
|
+
onCloseDetail={closeDetail}
|
|
168
|
+
expanded={view === "full"}
|
|
169
|
+
onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
170
|
+
detailWidth="xl"
|
|
171
|
+
/>
|
|
172
|
+
<FormSheet open={sheet === "record"} onOpenChange={(o) => setSheet(o ? "record" : null)} title="Record a message" description={`An email, LinkedIn message or call note that happened off ${brand.name}.`} width="lg">
|
|
173
|
+
{sheet === "record" ? <CorrespondenceForm accountId={accountId} stakeholder={person} onCancel={() => setSheet(null)} onSaved={(r) => { setSheet(null); void list.refetch(); setCid(r.id); }} /> : null}
|
|
174
|
+
</FormSheet>
|
|
175
|
+
<FormSheet open={sheet === "import"} onOpenChange={(o) => setSheet(o ? "import" : null)} title="Import messages" description="Bring a batch or a file in." width="lg">
|
|
176
|
+
{sheet === "import" ? <CorrespondenceImport accountId={accountId} maxBytes={maxBytes} onImported={() => void list.refetch()} onDone={() => setSheet(null)} /> : null}
|
|
177
|
+
</FormSheet>
|
|
178
|
+
<ConfirmSpendDialog
|
|
179
|
+
open={toDelete !== null}
|
|
180
|
+
tier="destructive"
|
|
181
|
+
title="Delete this message?"
|
|
182
|
+
affected={1}
|
|
183
|
+
description={<><p>The record is removed. Documents already generated from it are not rewritten.</p>{remove.error ? <ApiError error={remove.error} title="Could not delete the message" /> : null}</>}
|
|
184
|
+
confirmLabel="Delete"
|
|
185
|
+
confirming={remove.loading}
|
|
186
|
+
onCancel={() => setToDelete(null)}
|
|
187
|
+
onConfirm={() => { if (toDelete) void remove.mutate(toDelete); }}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DocumentsScreen: the finished documents workspace. No props needed.
|
|
5
|
+
*
|
|
6
|
+
* Composes: ScreenLayout (title, tabs), LibraryPanel (the organization's document Library), AccountBar + QuestionnairesPanel
|
|
7
|
+
* (an account's security questionnaires and their review) and AccountBar + CorrespondencePanel (what has been said to an account).
|
|
8
|
+
*
|
|
9
|
+
* Tabs (`?tab=`): Library, Security questionnaires, Correspondence. The two account tabs share one account (`?account=<company id>`),
|
|
10
|
+
* so a link opens straight to a company's paperwork.
|
|
11
|
+
*
|
|
12
|
+
* User flows: browse the Library by folder; search and filter it; open a document to read its text, its facts, download the
|
|
13
|
+
* original, edit it, read a PDF or Word file's text again, restore a removed one, or delete it (confirmed); make, rename and
|
|
14
|
+
* delete folders; write a text document; upload one or many files (checked against the Library's types and the proxy's size
|
|
15
|
+
* limit first). For an account: upload a buyer's questionnaire (confirmed, billable), follow it while it is answered, review every
|
|
16
|
+
* answer (edit, approve, escalate, mark not applicable, mark wrong, save to the answer library), download the filled workbook,
|
|
17
|
+
* rename or delete it (confirmed); record a message, import a batch or a file, filter by channel and person, delete (confirmed).
|
|
18
|
+
*
|
|
19
|
+
* SDK operations: none directly; every call lives in the composed parts, which confirm each billable and destructive action.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { FileText } from "lucide-react";
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
25
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
26
|
+
import { DEFAULT_MAX_BYTES } from "../lib/documents-types";
|
|
27
|
+
import type { LibraryDoc } from "../lib/documents-types";
|
|
28
|
+
import { AccountBar } from "./account-bar";
|
|
29
|
+
import { CorrespondencePanel } from "./correspondence-panel";
|
|
30
|
+
import { LibraryPanel } from "./library-panel";
|
|
31
|
+
import { QuestionnairesPanel } from "./questionnaires-panel";
|
|
32
|
+
|
|
33
|
+
export interface DocumentsScreenProps {
|
|
34
|
+
renderRowActions?: (doc: LibraryDoc) => ReactNode;
|
|
35
|
+
renderDetailActions?: (doc: LibraryDoc) => ReactNode;
|
|
36
|
+
extraDetailTabs?: { id: string; label: string; render: (doc: LibraryDoc) => ReactNode }[];
|
|
37
|
+
toolbarActions?: ReactNode;
|
|
38
|
+
/** Largest upload in bytes. Default 5 MiB, the /api/g8 proxy's body limit; raise `maxBodyBytes` in the proxy route first. */
|
|
39
|
+
maxBytes?: number;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Tab = "library" | "questionnaires" | "correspondence";
|
|
44
|
+
const TABS: Tab[] = ["library", "questionnaires", "correspondence"];
|
|
45
|
+
|
|
46
|
+
export function DocumentsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, maxBytes = DEFAULT_MAX_BYTES, className }: DocumentsScreenProps) {
|
|
47
|
+
const [tabParam, setTabParam] = useUrlState("tab", "library");
|
|
48
|
+
const [account, setAccount] = useUrlState("account", "");
|
|
49
|
+
const [, setId] = useUrlState("id", "");
|
|
50
|
+
const [, setView] = useUrlState("view", "");
|
|
51
|
+
const [, setQid] = useUrlState("qid", "");
|
|
52
|
+
const [, setCid] = useUrlState("cid", "");
|
|
53
|
+
const tab = (TABS.includes(tabParam as Tab) ? tabParam : "library") as Tab;
|
|
54
|
+
const accountId = account && Number.isInteger(Number(account)) && Number(account) > 0 ? Number(account) : null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ScreenLayout
|
|
58
|
+
className={className}
|
|
59
|
+
title="Documents"
|
|
60
|
+
description="Your document library, the security questionnaires buyers send, and the correspondence behind each account."
|
|
61
|
+
icon={<FileText className="size-5" />}
|
|
62
|
+
tabs={[{ id: "library", label: "Library" }, { id: "questionnaires", label: "Security questionnaires" }, { id: "correspondence", label: "Correspondence" }]}
|
|
63
|
+
activeTab={tab}
|
|
64
|
+
onTabChange={(next) => { setTabParam(next === "library" ? null : next); setId(null); setQid(null); setCid(null); setView(null); }}
|
|
65
|
+
>
|
|
66
|
+
{tab === "library" ? (
|
|
67
|
+
<LibraryPanel renderRowActions={renderRowActions} renderDetailActions={renderDetailActions} extraDetailTabs={extraDetailTabs} toolbarActions={toolbarActions} maxBytes={maxBytes} />
|
|
68
|
+
) : (
|
|
69
|
+
<div className="grid gap-4">
|
|
70
|
+
<AccountBar className="max-w-sm" accountId={accountId} onChange={(id) => { setAccount(String(id)); setQid(null); setCid(null); setView(null); }} />
|
|
71
|
+
{tab === "questionnaires" ? <QuestionnairesPanel accountId={accountId} toolbarActions={toolbarActions} maxBytes={maxBytes} /> : <CorrespondencePanel accountId={accountId} toolbarActions={toolbarActions} maxBytes={maxBytes} />}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</ScreenLayout>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LibraryDocumentDetail: one Library document with its text, its facts and every action on it.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getLibraryDocument (GET /library/documents/{document_id}) read the full record, with its extracted text
|
|
8
|
+
* 2. downloadLibraryDocument (GET /library/documents/{document_id}/download) read the ORIGINAL bytes (not the JSON envelope), saved as a file
|
|
9
|
+
* 3. updateLibraryDocument, through LibraryDocumentForm write rename, move or change the text
|
|
10
|
+
* 4. reextractLibraryDocument (POST .../reextract) write read a PDF or DOCX's text again; replaces the stored text, never the file
|
|
11
|
+
* 5. refreshLibraryDocument (POST .../refresh) write re-fetch a SCRAPED page (the API may answer that it cannot yet; its message is shown as is)
|
|
12
|
+
* 6. restoreLibraryDocument (POST .../restore) write bring back a removed document
|
|
13
|
+
* 7. deleteLibraryDocument (DELETE .../{document_id}) DESTRUCTIVE removes the row AND its bytes for good; behind ConfirmSpendDialog
|
|
14
|
+
* Only the owner or an admin can delete (the API says 403 otherwise and the reason is shown). Nothing here is billable or external.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import type { ReactNode } from "react";
|
|
19
|
+
import { Download, FileText, Pencil, RefreshCw, RotateCcw, ScanText, Trash2 } from "lucide-react";
|
|
20
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
25
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
26
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
27
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
28
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
30
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
31
|
+
import { downloadFile } from "../lib/download";
|
|
32
|
+
import { REEXTRACT_TYPES, docName, fmtSize, folderLabel, when } from "../lib/documents-types";
|
|
33
|
+
import type { LibraryDoc, LibraryFolder, Rec } from "../lib/documents-types";
|
|
34
|
+
import { LibraryDocumentForm } from "./library-document-form";
|
|
35
|
+
|
|
36
|
+
export interface LibraryDocumentDetailProps {
|
|
37
|
+
doc: LibraryDoc;
|
|
38
|
+
folders: LibraryFolder[];
|
|
39
|
+
onClose: () => void;
|
|
40
|
+
onDeleted?: (doc: LibraryDoc) => void;
|
|
41
|
+
onExpand?: () => void;
|
|
42
|
+
onCollapse?: () => void;
|
|
43
|
+
renderActions?: (doc: LibraryDoc) => ReactNode;
|
|
44
|
+
extraTabs?: { id: string; label: string; render: (doc: LibraryDoc) => ReactNode }[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const sourceLabel = (s: string | null | undefined): string => (s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
|
|
48
|
+
|
|
49
|
+
export function LibraryDocumentDetail({ doc, folders, onClose, onDeleted, onExpand, onCollapse, renderActions, extraTabs }: LibraryDocumentDetailProps) {
|
|
50
|
+
const [editing, setEditing] = useState(false);
|
|
51
|
+
const [deleting, setDeleting] = useState(false);
|
|
52
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
53
|
+
const [downloadError, setDownloadError] = useState<unknown>(undefined);
|
|
54
|
+
const [downloading, setDownloading] = useState(false);
|
|
55
|
+
|
|
56
|
+
const full = useG8Query(
|
|
57
|
+
(signal) => g8Get<LibraryDoc>(`/library/documents/${encodeURIComponent(doc.id)}`, {}, { signal }).then((r) => r.data),
|
|
58
|
+
[doc.id],
|
|
59
|
+
{ key: ["documents", "doc", doc.id], placeholderData: doc },
|
|
60
|
+
);
|
|
61
|
+
const d = full.data && full.data.id === doc.id ? full.data : doc;
|
|
62
|
+
const removed = Boolean(d.deleted_at);
|
|
63
|
+
const type = (d.file_type ?? "").toLowerCase();
|
|
64
|
+
const canReextract = (REEXTRACT_TYPES as readonly string[]).includes(type) && d.has_file_data === true && !removed;
|
|
65
|
+
|
|
66
|
+
const reextract = useG8Mutation(() => g8Post<Rec>(`/library/documents/${encodeURIComponent(doc.id)}/reextract`), {
|
|
67
|
+
onSuccess: (r) => { const n = (r.data as Rec | null)?.content_length; setNotice(typeof n === "number" ? `Text read again: ${n.toLocaleString()} characters.` : "Text read again."); },
|
|
68
|
+
invalidates: [["documents"]],
|
|
69
|
+
});
|
|
70
|
+
const refresh = useG8Mutation(() => g8Post<Rec>(`/library/documents/${encodeURIComponent(doc.id)}/refresh`), {
|
|
71
|
+
onSuccess: (r) => { const m = (r.data as Rec | null)?.message; setNotice(typeof m === "string" && m ? m : "Refresh requested."); },
|
|
72
|
+
invalidates: [["documents"]],
|
|
73
|
+
});
|
|
74
|
+
const restore = useG8Mutation(() => g8Post<LibraryDoc>(`/library/documents/${encodeURIComponent(doc.id)}/restore`), { onSuccess: () => setNotice("Document restored."), invalidates: [["documents"]] });
|
|
75
|
+
const remove = useG8Mutation(() => g8Delete(`/library/documents/${encodeURIComponent(doc.id)}`), {
|
|
76
|
+
onSuccess: () => { setDeleting(false); onDeleted?.(doc); },
|
|
77
|
+
invalidates: [["documents"]],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const save = async (): Promise<void> => {
|
|
81
|
+
setDownloading(true);
|
|
82
|
+
setDownloadError(undefined);
|
|
83
|
+
try {
|
|
84
|
+
await downloadFile(`/library/documents/${encodeURIComponent(doc.id)}/download`, d.file_name || docName(d));
|
|
85
|
+
} catch (err) {
|
|
86
|
+
setDownloadError(err);
|
|
87
|
+
} finally {
|
|
88
|
+
setDownloading(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const preview = full.error && !full.data ? <ApiError error={full.error} title="Could not load this document" onRetry={() => void full.refetch()} /> : full.isPlaceholderData ? (
|
|
93
|
+
<Skeleton className="h-32 w-full" aria-label="Loading text" />
|
|
94
|
+
) : d.content ? (
|
|
95
|
+
<pre aria-label="Document text" className="bg-muted max-h-[28rem] overflow-auto whitespace-pre-wrap rounded-md p-3 text-sm">{d.content}</pre>
|
|
96
|
+
) : (
|
|
97
|
+
<EmptyState compact icon={<FileText className="size-8" />} title="No text to show" description={type === "png" || type === "jpg" || type === "jpeg" ? "Images are stored as files. Download it to see it." : d.has_file_data ? "No text was read from this file. Download the original, or read it again." : "This document has no text."} />
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const actions = (
|
|
101
|
+
<>
|
|
102
|
+
{renderActions?.(d)}
|
|
103
|
+
{removed ? (
|
|
104
|
+
<Button size="sm" variant="outline" loading={restore.loading} onClick={() => void restore.mutate(undefined)}><RotateCcw className="size-4" />Restore</Button>
|
|
105
|
+
) : (
|
|
106
|
+
<Button size="sm" variant="outline" disabled={full.isPlaceholderData} onClick={() => setEditing(true)}><Pencil className="size-4" />Edit</Button>
|
|
107
|
+
)}
|
|
108
|
+
<Button size="sm" variant="outline" loading={downloading} onClick={() => void save()}><Download className="size-4" />Download</Button>
|
|
109
|
+
{canReextract ? <Button size="sm" variant="outline" loading={reextract.loading} onClick={() => void reextract.mutate(undefined)}><ScanText className="size-4" />Read text again</Button> : null}
|
|
110
|
+
{d.source === "scrape" && !removed ? <Button size="sm" variant="outline" loading={refresh.loading} onClick={() => void refresh.mutate(undefined)}><RefreshCw className="size-4" />Refresh from source</Button> : null}
|
|
111
|
+
<Button size="sm" variant="outline" onClick={() => setDeleting(true)}><Trash2 className="size-4" />Delete</Button>
|
|
112
|
+
</>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<DetailPanel
|
|
118
|
+
title={docName(d)}
|
|
119
|
+
subtitle={`${folderLabel(d.folder_path)}${d.file_name && d.file_name !== d.display_name ? ` · ${d.file_name}` : ""}`}
|
|
120
|
+
badges={<>{type ? <StatusPill tone="info">{type.toUpperCase()}</StatusPill> : null}{removed ? <StatusPill tone="warning">Removed</StatusPill> : null}</>}
|
|
121
|
+
actions={actions}
|
|
122
|
+
onClose={onClose}
|
|
123
|
+
onExpand={onExpand}
|
|
124
|
+
onCollapse={onCollapse}
|
|
125
|
+
summary={notice || reextract.error || refresh.error || restore.error || downloadError ? (
|
|
126
|
+
<div className="grid gap-2 px-4 pt-3">
|
|
127
|
+
{notice ? <p role="status" className="text-sm">{notice}</p> : null}
|
|
128
|
+
{reextract.error ? <ApiError error={reextract.error} title="Could not read the text again" compact /> : null}
|
|
129
|
+
{refresh.error ? <ApiError error={refresh.error} title="Could not refresh the document" compact /> : null}
|
|
130
|
+
{restore.error ? <ApiError error={restore.error} title="Could not restore the document" compact /> : null}
|
|
131
|
+
{downloadError ? <ApiError error={downloadError} title="Could not download the file" compact /> : null}
|
|
132
|
+
</div>
|
|
133
|
+
) : undefined}
|
|
134
|
+
tabs={[
|
|
135
|
+
{ id: "text", label: "Text", content: preview },
|
|
136
|
+
{
|
|
137
|
+
id: "details",
|
|
138
|
+
label: "Details",
|
|
139
|
+
content: (
|
|
140
|
+
<KeyValueList columns={2} items={[
|
|
141
|
+
{ label: "Title", value: d.display_name },
|
|
142
|
+
{ label: "File name", value: d.file_name },
|
|
143
|
+
{ label: "Type", value: type ? type.toUpperCase() : "" },
|
|
144
|
+
{ label: "Folder", value: folderLabel(d.folder_path) },
|
|
145
|
+
{ label: "Size", value: fmtSize(d.file_size_bytes) },
|
|
146
|
+
{ label: "Source", value: sourceLabel(d.source) },
|
|
147
|
+
{ label: "Added by", value: d.owner_name },
|
|
148
|
+
{ label: "Added", value: when(d.created_at) },
|
|
149
|
+
{ label: "Updated", value: when(d.updated_at) },
|
|
150
|
+
{ label: "Searchable by the AI", value: d.indexed_at ? `Since ${when(d.indexed_at)}` : "Not yet" },
|
|
151
|
+
{ label: "Version", value: d.current_version != null ? String(d.current_version) : "" },
|
|
152
|
+
{ label: "Document id", value: d.id, mono: true },
|
|
153
|
+
]} />
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
...(extraTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(d) })),
|
|
157
|
+
]}
|
|
158
|
+
/>
|
|
159
|
+
<FormSheet open={editing} onOpenChange={setEditing} title="Edit document" description="Only what you change is saved." width="lg">
|
|
160
|
+
{editing ? <LibraryDocumentForm document={doc} folders={folders} onCancel={() => setEditing(false)} onSaved={() => { setEditing(false); setNotice("Saved."); }} /> : null}
|
|
161
|
+
</FormSheet>
|
|
162
|
+
<ConfirmSpendDialog
|
|
163
|
+
open={deleting}
|
|
164
|
+
tier="destructive"
|
|
165
|
+
title={`Delete ${docName(d)}?`}
|
|
166
|
+
affected={1}
|
|
167
|
+
description={<><p>The document and its stored file are removed for good. It cannot be restored. Only its owner or an admin can delete it.</p>{remove.error ? <ApiError error={remove.error} title="Could not delete the document" /> : null}</>}
|
|
168
|
+
confirmLabel="Delete"
|
|
169
|
+
confirming={remove.loading}
|
|
170
|
+
onCancel={() => setDeleting(false)}
|
|
171
|
+
onConfirm={() => void remove.mutate(undefined)}
|
|
172
|
+
/>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|