@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,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LibraryDocumentForm: write a text document into the Library, or edit one (rename, move, change its text).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getLibraryDocument (GET /library/documents/{document_id}) read edit only: the FULL record is read before the form shows
|
|
8
|
+
* 2. createLibraryDocument (POST /library/documents) write new text document; stored as typed, no extraction
|
|
9
|
+
* 3. updateLibraryDocument (PUT /library/documents/{document_id}) write only the fields that changed; an omitted field is left alone
|
|
10
|
+
* Edit is disabled until the full record has loaded: a list row can carry less than the record, and saving a form seeded
|
|
11
|
+
* from it would write blanks over real text. A cleared text box is sent as an empty string, which the API stores. After a
|
|
12
|
+
* save the document's cache entries and the library lists are refreshed, so reopening the form shows what was saved.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { ROOT, docName, fileNameFor, folderLabel } from "../lib/documents-types";
|
|
22
|
+
import type { LibraryDoc, LibraryFolder } from "../lib/documents-types";
|
|
23
|
+
|
|
24
|
+
export interface LibraryDocumentFormProps {
|
|
25
|
+
/** Edit this document; omit to write a new one. */
|
|
26
|
+
document?: LibraryDoc;
|
|
27
|
+
folders: LibraryFolder[];
|
|
28
|
+
/** Pre-select the folder for a new document. */
|
|
29
|
+
folderPath?: string;
|
|
30
|
+
onSaved: (doc: LibraryDoc) => void;
|
|
31
|
+
onCancel?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
35
|
+
|
|
36
|
+
export function LibraryDocumentForm({ document, folders, folderPath, onSaved, onCancel }: LibraryDocumentFormProps) {
|
|
37
|
+
const full = useG8Query(
|
|
38
|
+
(signal) => g8Get<LibraryDoc>(`/library/documents/${encodeURIComponent(document!.id)}`, {}, { signal }).then((r) => r.data),
|
|
39
|
+
[document?.id],
|
|
40
|
+
{ enabled: Boolean(document), key: ["documents", "doc", document?.id ?? ""], placeholderData: document },
|
|
41
|
+
);
|
|
42
|
+
if (!document) return <Fields folders={folders} folderPath={folderPath} onSaved={onSaved} onCancel={onCancel} />;
|
|
43
|
+
if (full.error && (!full.data || full.isPlaceholderData)) return <ApiError error={full.error} title="Could not load this document" onRetry={() => void full.refetch()} />;
|
|
44
|
+
if (!full.data || full.isPlaceholderData || full.data.id !== document.id) return <Skeleton className="h-40 w-full" aria-label="Loading document" />;
|
|
45
|
+
return <Fields key={full.data.id} document={full.data} folders={folders} onSaved={onSaved} onCancel={onCancel} />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Fields({ document, folders, folderPath, onSaved, onCancel }: LibraryDocumentFormProps) {
|
|
49
|
+
const editing = Boolean(document);
|
|
50
|
+
const [title, setTitle] = useState(document?.display_name ?? "");
|
|
51
|
+
const [fileName, setFileName] = useState(document?.file_name ?? "");
|
|
52
|
+
const [fileTouched, setFileTouched] = useState(false);
|
|
53
|
+
const [folder, setFolder] = useState(document?.folder_path ?? folderPath ?? ROOT);
|
|
54
|
+
const [content, setContent] = useState(document?.content ?? "");
|
|
55
|
+
const folderChoices = [...new Set([ROOT, ...folders.map((f) => f.full_path), folder])];
|
|
56
|
+
const valid = title.trim() !== "" && (editing || (fileName.trim() !== "" && content.trim() !== ""));
|
|
57
|
+
|
|
58
|
+
const save = useG8Mutation(
|
|
59
|
+
async (): Promise<LibraryDoc> => {
|
|
60
|
+
if (document) {
|
|
61
|
+
const diff: Record<string, unknown> = {};
|
|
62
|
+
if (title.trim() !== (document.display_name ?? "")) diff.display_name = title.trim();
|
|
63
|
+
if (folder !== document.folder_path) diff.folder_path = folder;
|
|
64
|
+
if (content !== (document.content ?? "")) diff.content = content;
|
|
65
|
+
if (Object.keys(diff).length === 0) return document;
|
|
66
|
+
return (await g8Put<LibraryDoc>(`/library/documents/${encodeURIComponent(document.id)}`, diff)).data;
|
|
67
|
+
}
|
|
68
|
+
return (await g8Post<LibraryDoc>("/library/documents", { folder_path: folder, file_name: fileName.trim(), display_name: title.trim(), content })).data;
|
|
69
|
+
},
|
|
70
|
+
{ onSuccess: onSaved, invalidates: (saved) => [["documents", "list"], ["documents", "tree"], ["documents", "doc", saved?.id ?? document?.id ?? ""]] },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (valid) void save.mutate(undefined); }}>
|
|
75
|
+
<div>
|
|
76
|
+
<Label htmlFor="g8-dd-title" className="mb-1">Title</Label>
|
|
77
|
+
<Input id="g8-dd-title" value={title} onChange={(e) => { setTitle(e.target.value); if (!editing && !fileTouched) setFileName(fileNameFor(e.target.value)); }} placeholder="Security overview" />
|
|
78
|
+
</div>
|
|
79
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
80
|
+
<div>
|
|
81
|
+
<Label htmlFor="g8-dd-folder" className="mb-1">Folder</Label>
|
|
82
|
+
<select id="g8-dd-folder" className={SELECT} value={folder} onChange={(e) => setFolder(e.target.value)}>
|
|
83
|
+
{folderChoices.map((p) => <option key={p} value={p}>{folderLabel(p)}</option>)}
|
|
84
|
+
</select>
|
|
85
|
+
</div>
|
|
86
|
+
{editing ? null : (
|
|
87
|
+
<div>
|
|
88
|
+
<Label htmlFor="g8-dd-file" className="mb-1">File name</Label>
|
|
89
|
+
<Input id="g8-dd-file" value={fileName} onChange={(e) => { setFileName(e.target.value); setFileTouched(true); }} placeholder="security-overview.md" />
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<Label htmlFor="g8-dd-content" className="mb-1">Text</Label>
|
|
95
|
+
<Textarea id="g8-dd-content" rows={12} value={content} onChange={(e) => setContent(e.target.value)} placeholder={editing ? undefined : "Write or paste the document. It is stored as typed."} />
|
|
96
|
+
{editing && document?.has_file_data ? <p className="text-muted-foreground mt-1 text-xs">{docName(document)} was uploaded as a file. Changing this text changes what the library reads; the original file is untouched.</p> : null}
|
|
97
|
+
</div>
|
|
98
|
+
{save.error ? <ApiError error={save.error} title="Could not save the document" /> : null}
|
|
99
|
+
<div className="flex gap-2">
|
|
100
|
+
<Button type="submit" loading={save.loading} disabled={!valid}>{editing ? "Save changes" : "Add document"}</Button>
|
|
101
|
+
{onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
|
|
102
|
+
</div>
|
|
103
|
+
</form>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LibraryFolderForm: make a Library folder or rename one.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. createLibraryFolder (POST /library/folders) write a new folder under a parent (the Library root or another folder)
|
|
8
|
+
* 2. renameLibraryFolder (PUT /library/folders/{folder_id}) write rename only; the documents inside keep their ids
|
|
9
|
+
* The API's rules are checked here first so the person sees them before the call: a name is letters, numbers, spaces,
|
|
10
|
+
* hyphens and underscores (at most 50), and folders go three levels deep. Nothing here is billable, external or destructive.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
17
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
18
|
+
import { ROOT, folderLabel, folderNameProblem } from "../lib/documents-types";
|
|
19
|
+
import type { LibraryFolder } from "../lib/documents-types";
|
|
20
|
+
|
|
21
|
+
export interface LibraryFolderFormProps {
|
|
22
|
+
/** Rename this folder; omit to create one. */
|
|
23
|
+
folder?: LibraryFolder;
|
|
24
|
+
/** Every folder, to offer as the parent when creating. */
|
|
25
|
+
folders: LibraryFolder[];
|
|
26
|
+
/** Pre-select the parent when creating. */
|
|
27
|
+
parentPath?: string;
|
|
28
|
+
onSaved: (folder: LibraryFolder) => void;
|
|
29
|
+
onCancel?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
33
|
+
|
|
34
|
+
export function LibraryFolderForm({ folder, folders, parentPath, onSaved, onCancel }: LibraryFolderFormProps) {
|
|
35
|
+
const [name, setName] = useState(folder?.name ?? "");
|
|
36
|
+
const [parent, setParent] = useState(parentPath || ROOT);
|
|
37
|
+
const parentOptions = [ROOT, ...folders.map((f) => f.full_path)].filter((p) => p.split("/").length < 3);
|
|
38
|
+
const problem = name.trim() === "" ? null : folderNameProblem(name, folder ? folder.parent_path : parent);
|
|
39
|
+
const changed = folder ? name.trim() !== folder.name : true;
|
|
40
|
+
const valid = name.trim() !== "" && problem === null && changed;
|
|
41
|
+
|
|
42
|
+
const save = useG8Mutation(
|
|
43
|
+
async (): Promise<LibraryFolder> => {
|
|
44
|
+
if (folder) return (await g8Put<LibraryFolder>(`/library/folders/${encodeURIComponent(folder.id)}`, { name: name.trim() })).data;
|
|
45
|
+
return (await g8Post<LibraryFolder>("/library/folders", { name: name.trim(), parent_path: parent })).data;
|
|
46
|
+
},
|
|
47
|
+
{ onSuccess: onSaved, invalidates: [["documents"]] },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (valid) void save.mutate(undefined); }}>
|
|
52
|
+
<div>
|
|
53
|
+
<Label htmlFor="g8-df-name" className="mb-1">Folder name</Label>
|
|
54
|
+
<Input id="g8-df-name" value={name} maxLength={50} onChange={(e) => setName(e.target.value)} placeholder="Case studies" />
|
|
55
|
+
{problem ? <p role="alert" className="text-error mt-1 text-xs">{problem}</p> : null}
|
|
56
|
+
</div>
|
|
57
|
+
{folder ? (
|
|
58
|
+
<p className="text-muted-foreground text-xs">Inside {folderLabel(folder.parent_path)}. Renaming keeps every document in it.</p>
|
|
59
|
+
) : (
|
|
60
|
+
<div>
|
|
61
|
+
<Label htmlFor="g8-df-parent" className="mb-1">Inside</Label>
|
|
62
|
+
<select id="g8-df-parent" className={SELECT} value={parent} onChange={(e) => setParent(e.target.value)}>
|
|
63
|
+
{parentOptions.map((p) => <option key={p} value={p}>{folderLabel(p)}</option>)}
|
|
64
|
+
</select>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
{save.error ? <ApiError error={save.error} title={folder ? "Could not rename the folder" : "Could not create the folder"} /> : null}
|
|
68
|
+
<div className="flex gap-2">
|
|
69
|
+
<Button type="submit" loading={save.loading} disabled={!valid}>{folder ? "Rename folder" : "Create folder"}</Button>
|
|
70
|
+
{onCancel ? <Button type="button" variant="outline" onClick={onCancel}>Cancel</Button> : null}
|
|
71
|
+
</div>
|
|
72
|
+
</form>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LibraryPanel: the organization's document Library as a finished page: folders on one side, documents and the selected
|
|
5
|
+
* document's detail on the other.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. getLibraryTree (GET /library/tree) read read FIRST: the API creates the preset folders (Security, Case Studies) when the tree is read
|
|
9
|
+
* 2. listLibraryFolders (GET /library/folders) read the flat folder list (the tree leaves out folders more than one level down)
|
|
10
|
+
* 3. listLibraryDocuments (GET /library/documents) read every document, or with removed ones too; the API does not page it
|
|
11
|
+
* 4. deleteLibraryFolder (DELETE /library/folders/{folder_id}) DESTRUCTIVE an EMPTY folder only; behind ConfirmSpendDialog
|
|
12
|
+
* 5. reextractAllLibraryDocuments (POST .../reextract-all) write replaces the stored text of PDF and Word files (never the files); behind ConfirmSpendDialog, counted from the documents it will touch
|
|
13
|
+
* New folders, renames, new text documents, uploads and every per-document action live in the composed parts
|
|
14
|
+
* (LibraryFolderForm, LibraryDocumentForm, LibraryUpload, LibraryDocumentDetail). The Library here is the organization's
|
|
15
|
+
* (global) one: the API's campaign and account libraries are read and written with different ids, so they are not offered.
|
|
16
|
+
* URL: `?id=` the selected document, `?folder=` the folder, `&view=full` the document as a page.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import type { ReactNode } from "react";
|
|
21
|
+
import { FileText, Folder, FolderOpen, FolderPlus, Pencil, Plus, ScanText, Trash2, Upload } from "lucide-react";
|
|
22
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
23
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
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 { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
28
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
29
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
30
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
31
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
32
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
33
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
34
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
35
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
36
|
+
import { plural } from "@/g8/lib/plural";
|
|
37
|
+
import { DEFAULT_MAX_BYTES, REEXTRACT_TYPES, ROOT, docName, fmtSize, folderLabel, rowsOf, when } from "../lib/documents-types";
|
|
38
|
+
import type { LibraryDoc, LibraryFolder, Rec } from "../lib/documents-types";
|
|
39
|
+
import { LibraryDocumentDetail } from "./library-document-detail";
|
|
40
|
+
import { LibraryDocumentForm } from "./library-document-form";
|
|
41
|
+
import { LibraryFolderForm } from "./library-folder-form";
|
|
42
|
+
import { LibraryUpload } from "./library-upload";
|
|
43
|
+
|
|
44
|
+
export interface LibraryPanelProps {
|
|
45
|
+
renderRowActions?: (doc: LibraryDoc) => ReactNode;
|
|
46
|
+
renderDetailActions?: (doc: LibraryDoc) => ReactNode;
|
|
47
|
+
extraDetailTabs?: { id: string; label: string; render: (doc: LibraryDoc) => ReactNode }[];
|
|
48
|
+
toolbarActions?: ReactNode;
|
|
49
|
+
/** Largest upload in bytes. Default 5 MiB, the /api/g8 proxy's body limit. */
|
|
50
|
+
maxBytes?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Sheet = null | "upload" | "document" | "folder" | "rename";
|
|
54
|
+
const SELECT = "border-input bg-background h-9 rounded-md border px-2 text-sm";
|
|
55
|
+
const ALL = "";
|
|
56
|
+
|
|
57
|
+
/** The documents the "read text again" call will touch: PDF and Word files with stored bytes, in the folder and under it (the API matches on the path prefix). */
|
|
58
|
+
export function reextractTargets(docs: LibraryDoc[], folderPath: string): LibraryDoc[] {
|
|
59
|
+
return docs.filter((d) => !d.deleted_at && d.has_file_data === true && (REEXTRACT_TYPES as readonly string[]).includes((d.file_type ?? "").toLowerCase()) && (folderPath === ALL || d.folder_path.startsWith(folderPath)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function LibraryPanel({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, maxBytes = DEFAULT_MAX_BYTES }: LibraryPanelProps) {
|
|
63
|
+
const brand = useBrand();
|
|
64
|
+
const [id, setId] = useUrlState("id", "");
|
|
65
|
+
const [folderParam, setFolderParam] = useUrlState("folder", "");
|
|
66
|
+
const [view, setView] = useUrlState("view", "");
|
|
67
|
+
const [search, setSearch] = useState("");
|
|
68
|
+
const [typeFilter, setTypeFilter] = useState("");
|
|
69
|
+
const [sourceFilter, setSourceFilter] = useState("");
|
|
70
|
+
const [showRemoved, setShowRemoved] = useState(false);
|
|
71
|
+
const [sheet, setSheet] = useState<Sheet>(null);
|
|
72
|
+
const [folderToDelete, setFolderToDelete] = useState<LibraryFolder | null>(null);
|
|
73
|
+
const [rereading, setRereading] = useState(false);
|
|
74
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
75
|
+
|
|
76
|
+
const tree = useG8Query((signal) => g8Get<Rec>("/library/tree", {}, { signal }).then((r) => r.data), [], { key: ["documents", "tree"] });
|
|
77
|
+
const ready = tree.data !== undefined || tree.error !== undefined;
|
|
78
|
+
const foldersQ = useG8Query((signal) => g8Get<Rec>("/library/folders", {}, { signal }).then((r) => rowsOf<LibraryFolder>(r.data)), [], { enabled: ready, key: ["documents", "folders"] });
|
|
79
|
+
const docsQ = useG8Query(
|
|
80
|
+
(signal) => g8Get<Rec>("/library/documents", { include_deleted: showRemoved ? true : undefined }, { signal }).then((r) => rowsOf<LibraryDoc>(r.data)),
|
|
81
|
+
[showRemoved],
|
|
82
|
+
{ key: ["documents", "list", showRemoved] },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const folders = (foldersQ.data ?? []).slice().sort((a, b) => a.full_path.localeCompare(b.full_path));
|
|
86
|
+
const docs = docsQ.data ?? [];
|
|
87
|
+
const folder = folders.find((f) => f.full_path === folderParam) ?? null;
|
|
88
|
+
const folderPath = folder ? folder.full_path : ALL;
|
|
89
|
+
const q = search.trim().toLowerCase();
|
|
90
|
+
const shown = docs.filter((d) => (folderPath === ALL || d.folder_path === folderPath) && (!typeFilter || (d.file_type ?? "") === typeFilter) && (!sourceFilter || (d.source ?? "") === sourceFilter) && (!q || `${docName(d)} ${d.file_name ?? ""} ${d.folder_path}`.toLowerCase().includes(q)));
|
|
91
|
+
const selected = docs.find((d) => d.id === id) ?? null;
|
|
92
|
+
const countIn = (path: string): number => docs.filter((d) => d.folder_path === path && !d.deleted_at).length;
|
|
93
|
+
const hasChildren = (f: LibraryFolder): boolean => folders.some((x) => x.parent_path === f.full_path);
|
|
94
|
+
const targets = reextractTargets(docs, folderPath);
|
|
95
|
+
const types = [...new Set(docs.map((d) => d.file_type).filter((t): t is string => Boolean(t)))].sort();
|
|
96
|
+
const sources = [...new Set(docs.map((d) => d.source).filter((s): s is string => Boolean(s)))].sort();
|
|
97
|
+
const known = docs.map((d) => d.file_size_bytes).filter((n): n is number => typeof n === "number");
|
|
98
|
+
const closeDetail = (): void => { setId(null); setView(null); };
|
|
99
|
+
|
|
100
|
+
const deleteFolder = useG8Mutation((f: LibraryFolder) => g8Delete(`/library/folders/${encodeURIComponent(f.id)}`), {
|
|
101
|
+
onSuccess: (_r, f) => { setFolderToDelete(null); if (folderParam === f.full_path) setFolderParam(null); },
|
|
102
|
+
invalidates: [["documents"]],
|
|
103
|
+
});
|
|
104
|
+
const rereadAll = useG8Mutation(() => g8Post<Rec>("/library/documents/reextract-all", undefined, { query: { folder_path: folder?.full_path } }), {
|
|
105
|
+
onSuccess: () => { setRereading(false); setNotice(`Asked the library to read the text of ${plural(targets.length, "document")} again.`); },
|
|
106
|
+
invalidates: [["documents"]],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const rail = (
|
|
110
|
+
<nav aria-label="Folders" className="grid gap-0.5 text-sm">
|
|
111
|
+
<button type="button" aria-current={folderPath === ALL} className={`flex items-center justify-between rounded-md px-2 py-1.5 text-left ${folderPath === ALL ? "bg-muted font-medium" : "hover:bg-muted"}`} onClick={() => setFolderParam(null)}>
|
|
112
|
+
<span className="flex items-center gap-2"><FolderOpen className="size-4" />All documents</span><span className="text-muted-foreground tabular-nums">{docsQ.data ? docs.filter((d) => !d.deleted_at).length : ""}</span>
|
|
113
|
+
</button>
|
|
114
|
+
{folders.map((f) => (
|
|
115
|
+
<button key={f.id} type="button" aria-current={folderPath === f.full_path} style={{ paddingLeft: `${0.5 + (f.full_path.split("/").length - 2) * 0.9}rem` }} className={`flex items-center justify-between rounded-md py-1.5 pr-2 text-left ${folderPath === f.full_path ? "bg-muted font-medium" : "hover:bg-muted"}`} onClick={() => setFolderParam(f.full_path)}>
|
|
116
|
+
<span className="flex min-w-0 items-center gap-2"><Folder className="size-4 shrink-0" /><span className="truncate">{f.name}</span></span><span className="text-muted-foreground tabular-nums">{docsQ.data ? countIn(f.full_path) : ""}</span>
|
|
117
|
+
</button>
|
|
118
|
+
))}
|
|
119
|
+
{foldersQ.error ? <ApiError error={foldersQ.error} title="Could not load folders" compact onRetry={() => void foldersQ.refetch()} /> : null}
|
|
120
|
+
</nav>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const rows = docsQ.error ? <ApiError error={docsQ.error} title="Could not load documents" onRetry={() => void docsQ.refetch()} /> : docsQ.loading && !docsQ.data ? (
|
|
124
|
+
<div aria-busy="true" className="grid gap-2"><Skeleton className="h-14 w-full" /><Skeleton className="h-14 w-full" /><Skeleton className="h-14 w-full" /></div>
|
|
125
|
+
) : shown.length === 0 ? (
|
|
126
|
+
<EmptyState icon={<FileText className="size-8" />} title={docs.length === 0 ? "Nothing in the library yet" : "No documents match"} description={docs.length === 0 ? `Add the material ${brand.name} should draw on: case studies, security policies, battlecards.` : folder && !q && !typeFilter && !sourceFilter ? `${folderLabel(folderPath)} has no documents.` : "Try another search or filter."} action={docs.length === 0 ? <Button onClick={() => setSheet("upload")}>Upload files</Button> : undefined} />
|
|
127
|
+
) : (
|
|
128
|
+
<>
|
|
129
|
+
<p role="status" className="text-muted-foreground mb-2 text-xs">{shown.length === docs.length ? `All ${plural(docs.length, "document")}` : `${shown.length.toLocaleString()} of ${plural(docs.length, "document")}`}</p>
|
|
130
|
+
<ul className="grid gap-2">
|
|
131
|
+
{shown.map((d) => (
|
|
132
|
+
<li key={d.id} className={`flex flex-wrap items-center justify-between gap-2 rounded-md border p-3 ${d.id === id ? "bg-muted" : ""}`}>
|
|
133
|
+
<button type="button" className="min-w-0 flex-1 text-left" onClick={() => setId(d.id)}>
|
|
134
|
+
<span className="block truncate font-medium">{docName(d)}</span>
|
|
135
|
+
<span className="text-muted-foreground block truncate text-xs">{folderLabel(d.folder_path)} · {fmtSize(d.file_size_bytes) || "-"} · {when(d.updated_at ?? d.created_at) || "-"}</span>
|
|
136
|
+
</button>
|
|
137
|
+
{d.deleted_at ? <StatusPill tone="warning">Removed</StatusPill> : null}
|
|
138
|
+
{d.file_type ? <StatusPill tone="info">{d.file_type.toUpperCase()}</StatusPill> : null}
|
|
139
|
+
{renderRowActions?.(d)}
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const list = (
|
|
147
|
+
<div className="grid gap-3">
|
|
148
|
+
<Toolbar
|
|
149
|
+
search={{ value: search, onChange: setSearch, placeholder: "Search documents" }}
|
|
150
|
+
filters={(
|
|
151
|
+
<>
|
|
152
|
+
<select aria-label="Filter by type" className={SELECT} value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}><option value="">All types</option>{types.map((t) => <option key={t} value={t}>{t.toUpperCase()}</option>)}</select>
|
|
153
|
+
<select aria-label="Filter by source" className={SELECT} value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)}><option value="">All sources</option>{sources.map((s) => <option key={s} value={s}>{s.charAt(0).toUpperCase() + s.slice(1)}</option>)}</select>
|
|
154
|
+
<label className="flex items-center gap-1.5 text-sm"><input type="checkbox" checked={showRemoved} onChange={(e) => { setShowRemoved(e.target.checked); closeDetail(); }} />Show removed</label>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
actions={(
|
|
158
|
+
<>
|
|
159
|
+
{toolbarActions}
|
|
160
|
+
<Button size="sm" variant="outline" onClick={() => setSheet("folder")}><FolderPlus className="size-4" />New folder</Button>
|
|
161
|
+
<Button size="sm" variant="outline" onClick={() => setSheet("document")}><Plus className="size-4" />New document</Button>
|
|
162
|
+
<Button size="sm" onClick={() => setSheet("upload")}><Upload className="size-4" />Upload</Button>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
/>
|
|
166
|
+
<div className="grid gap-3 lg:grid-cols-[14rem_1fr]">
|
|
167
|
+
<div className="grid content-start gap-2">
|
|
168
|
+
{rail}
|
|
169
|
+
{folder ? (
|
|
170
|
+
<div className="flex flex-wrap gap-1">
|
|
171
|
+
<Button size="sm" variant="ghost" onClick={() => setSheet("rename")}><Pencil className="size-4" />Rename folder</Button>
|
|
172
|
+
<Button size="sm" variant="ghost" disabled={countIn(folder.full_path) > 0 || hasChildren(folder)} title={countIn(folder.full_path) > 0 || hasChildren(folder) ? "Only an empty folder can be deleted: move or delete what is inside first." : undefined} onClick={() => setFolderToDelete(folder)}><Trash2 className="size-4" />Delete folder</Button>
|
|
173
|
+
</div>
|
|
174
|
+
) : null}
|
|
175
|
+
<Button size="sm" variant="ghost" disabled={targets.length === 0} title={targets.length === 0 ? "No PDF or Word files here to read again." : undefined} onClick={() => setRereading(true)}><ScanText className="size-4" />{folder ? "Read this folder's text again" : "Read all text again"}</Button>
|
|
176
|
+
</div>
|
|
177
|
+
<div className="min-w-0">{notice ? <p role="status" className="mb-2 text-sm">{notice}</p> : null}{rows}</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="grid gap-4">
|
|
184
|
+
<StatStrip items={[
|
|
185
|
+
{ label: "Documents", value: docsQ.data ? docs.filter((d) => !d.deleted_at).length.toLocaleString() : "..." },
|
|
186
|
+
{ label: "Folders", value: foldersQ.data ? folders.length.toLocaleString() : "..." },
|
|
187
|
+
{ label: "Stored", value: known.length > 0 ? fmtSize(known.reduce((a, b) => a + b, 0)) : "-", hint: known.length < docs.length ? "Adds up the documents that report a size." : undefined },
|
|
188
|
+
{ label: "Removed", value: showRemoved ? docs.filter((d) => d.deleted_at).length.toLocaleString() : "-", hint: showRemoved ? undefined : "Turn on Show removed to count them." },
|
|
189
|
+
]} />
|
|
190
|
+
<SplitView
|
|
191
|
+
list={list}
|
|
192
|
+
detail={selected ? <LibraryDocumentDetail key={selected.id} doc={selected} folders={folders} onClose={closeDetail} onDeleted={closeDetail} onExpand={() => setView("full")} onCollapse={view === "full" ? () => setView(null) : undefined} renderActions={renderDetailActions} extraTabs={extraDetailTabs} /> : null}
|
|
193
|
+
detailOpen={Boolean(selected)}
|
|
194
|
+
onCloseDetail={closeDetail}
|
|
195
|
+
expanded={view === "full"}
|
|
196
|
+
onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
197
|
+
detailWidth="xl"
|
|
198
|
+
/>
|
|
199
|
+
<FormSheet open={sheet === "upload"} onOpenChange={(o) => setSheet(o ? "upload" : null)} title="Upload files" description="Added to the folder you choose and read for their text.">
|
|
200
|
+
{sheet === "upload" ? <LibraryUpload folders={folders} folderPath={folderPath || ROOT} maxBytes={maxBytes} onDone={() => setSheet(null)} /> : null}
|
|
201
|
+
</FormSheet>
|
|
202
|
+
<FormSheet open={sheet === "document"} onOpenChange={(o) => setSheet(o ? "document" : null)} title="New document" description="Text you write or paste. It is stored as typed." width="lg">
|
|
203
|
+
{sheet === "document" ? <LibraryDocumentForm folders={folders} folderPath={folderPath || ROOT} onCancel={() => setSheet(null)} onSaved={(d) => { setSheet(null); setId(d.id); }} /> : null}
|
|
204
|
+
</FormSheet>
|
|
205
|
+
<FormSheet open={sheet === "folder"} onOpenChange={(o) => setSheet(o ? "folder" : null)} title="New folder">
|
|
206
|
+
{sheet === "folder" ? <LibraryFolderForm folders={folders} parentPath={folderPath || ROOT} onCancel={() => setSheet(null)} onSaved={(f) => { setSheet(null); setFolderParam(f.full_path); }} /> : null}
|
|
207
|
+
</FormSheet>
|
|
208
|
+
<FormSheet open={sheet === "rename" && folder !== null} onOpenChange={(o) => setSheet(o ? "rename" : null)} title="Rename folder">
|
|
209
|
+
{sheet === "rename" && folder ? <LibraryFolderForm folder={folder} folders={folders} onCancel={() => setSheet(null)} onSaved={(f) => { setSheet(null); setFolderParam(f.full_path ?? folder.full_path); }} /> : null}
|
|
210
|
+
</FormSheet>
|
|
211
|
+
<ConfirmSpendDialog
|
|
212
|
+
open={folderToDelete !== null}
|
|
213
|
+
tier="destructive"
|
|
214
|
+
title={`Delete the folder ${folderToDelete?.name ?? ""}?`}
|
|
215
|
+
affected={1}
|
|
216
|
+
description={<><p>An empty folder is removed. A folder with documents in it cannot be deleted.</p>{deleteFolder.error ? <ApiError error={deleteFolder.error} title="Could not delete the folder" /> : null}</>}
|
|
217
|
+
confirmLabel="Delete"
|
|
218
|
+
confirming={deleteFolder.loading}
|
|
219
|
+
onCancel={() => setFolderToDelete(null)}
|
|
220
|
+
onConfirm={() => { if (folderToDelete) void deleteFolder.mutate(folderToDelete); }}
|
|
221
|
+
/>
|
|
222
|
+
<ConfirmSpendDialog
|
|
223
|
+
open={rereading}
|
|
224
|
+
tier="destructive"
|
|
225
|
+
title={`Read the text of ${plural(targets.length, "document")} again?`}
|
|
226
|
+
affected={targets.length}
|
|
227
|
+
description={<><p>{folder ? `PDF and Word files in ${folderLabel(folder.full_path)} and under it` : "Every PDF and Word file in the library"} get their stored text replaced by a fresh read, including text edited by hand. The files themselves are not changed. A large library takes a while.</p>{rereadAll.error ? <ApiError error={rereadAll.error} title="Could not start the re-read" /> : null}</>}
|
|
228
|
+
confirmLabel="Read again"
|
|
229
|
+
confirming={rereadAll.loading}
|
|
230
|
+
onCancel={() => setRereading(false)}
|
|
231
|
+
onConfirm={() => void rereadAll.mutate(undefined)}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LibraryUpload: add files to a Library folder.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. uploadLibraryDocument (POST /library/documents/upload) write one file (multipart)
|
|
8
|
+
* 2. uploadLibraryDocuments (POST /library/documents/upload-batch) write several files at once; PARTIAL SUCCESS is normal
|
|
9
|
+
* Files are checked here before anything is sent: only the Library's file types, nothing empty, no file over `maxBytes`
|
|
10
|
+
* (default 5 MiB, the largest BODY the /api/g8 proxy forwards; the app itself takes 50 MB, so raise `maxBodyBytes` in the
|
|
11
|
+
* proxy route first), and at most 20 at a time. The proxy caps the whole request, not each file, so the files are sent in
|
|
12
|
+
* consecutive requests whose combined size (plus multipart overhead) stays under `maxBytes`; a lone file goes through the
|
|
13
|
+
* single route. Results are merged. A request that fails (a 413, a network error) marks only ITS files as failed and the later
|
|
14
|
+
* ones are still tried. The files that failed stay selected so a retry sends only them, never the ones that already landed. Text-bearing files are read for their text; images and
|
|
15
|
+
* spreadsheets are stored as bytes. Nothing here is billable, external or destructive.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useRef, useState } from "react";
|
|
19
|
+
import { Button, Label } from "@graph8/sdk/ui";
|
|
20
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { plural } from "@/g8/lib/plural";
|
|
23
|
+
import { BASE_OVERHEAD, DEFAULT_MAX_BYTES, FILE_OVERHEAD, LIBRARY_EXTENSIONS, fileLimit, ROOT, checkFiles, fmtSize, folderLabel } from "../lib/documents-types";
|
|
24
|
+
import type { LibraryFolder, Rec } from "../lib/documents-types";
|
|
25
|
+
|
|
26
|
+
export const MAX_BATCH = 20;
|
|
27
|
+
|
|
28
|
+
/** Consecutive groups of files whose combined size and multipart overhead stay within `maxBytes` (a file over the limit on its own still gets a group of its own, to be refused by the proxy with a reason). */
|
|
29
|
+
export function requestGroups(files: readonly File[], maxBytes: number): File[][] {
|
|
30
|
+
const groups: File[][] = [];
|
|
31
|
+
let current: File[] = [];
|
|
32
|
+
let size = BASE_OVERHEAD;
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
const cost = f.size + FILE_OVERHEAD;
|
|
35
|
+
if (current.length > 0 && size + cost > maxBytes) {
|
|
36
|
+
groups.push(current);
|
|
37
|
+
current = [];
|
|
38
|
+
size = BASE_OVERHEAD;
|
|
39
|
+
}
|
|
40
|
+
current.push(f);
|
|
41
|
+
size += cost;
|
|
42
|
+
}
|
|
43
|
+
if (current.length > 0) groups.push(current);
|
|
44
|
+
return groups;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LibraryUploadProps {
|
|
48
|
+
folders: LibraryFolder[];
|
|
49
|
+
/** Pre-select the destination folder. */
|
|
50
|
+
folderPath?: string;
|
|
51
|
+
/** Largest file accepted, in bytes. Default 5 MiB: the /api/g8 proxy rejects bigger bodies. */
|
|
52
|
+
maxBytes?: number;
|
|
53
|
+
onUploaded?: () => void;
|
|
54
|
+
onDone?: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface Outcome { added: number; failed: { filename: string; error: string }[]; warnings: string[] }
|
|
58
|
+
const reasonOf = (err: unknown): string => (err instanceof Error && err.message ? err.message : "The request failed.");
|
|
59
|
+
|
|
60
|
+
const SELECT = "border-input bg-background h-9 w-full rounded-md border px-2 text-sm";
|
|
61
|
+
|
|
62
|
+
export function LibraryUpload({ folders, folderPath, maxBytes = DEFAULT_MAX_BYTES, onUploaded, onDone }: LibraryUploadProps) {
|
|
63
|
+
const [folder, setFolder] = useState(folderPath || ROOT);
|
|
64
|
+
const [picked, setPicked] = useState<File[]>([]);
|
|
65
|
+
const [notes, setNotes] = useState<string[]>([]);
|
|
66
|
+
const [outcome, setOutcome] = useState<Outcome | null>(null);
|
|
67
|
+
const input = useRef<HTMLInputElement>(null);
|
|
68
|
+
const choices = [...new Set([ROOT, ...folders.map((f) => f.full_path), folder])];
|
|
69
|
+
|
|
70
|
+
const choose = (list: FileList | null): void => {
|
|
71
|
+
const { ok, problems } = checkFiles(Array.from(list ?? []), LIBRARY_EXTENSIONS, fileLimit(maxBytes));
|
|
72
|
+
const over = ok.length > MAX_BATCH ? [`Add at most ${MAX_BATCH} files at a time; the first ${MAX_BATCH} are kept.`] : [];
|
|
73
|
+
setPicked(ok.slice(0, MAX_BATCH));
|
|
74
|
+
setNotes([...problems, ...over]);
|
|
75
|
+
setOutcome(null);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const send = useG8Mutation(
|
|
79
|
+
async (files: File[]): Promise<Outcome> => {
|
|
80
|
+
const total: Outcome = { added: 0, failed: [], warnings: [] };
|
|
81
|
+
for (const group of requestGroups(files, maxBytes)) {
|
|
82
|
+
try {
|
|
83
|
+
const part = await sendGroup(group, folder);
|
|
84
|
+
total.added += part.added;
|
|
85
|
+
total.failed.push(...part.failed);
|
|
86
|
+
total.warnings.push(...part.warnings);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// One request failing (a 413, a network error) fails only its own files; the next group is still tried.
|
|
89
|
+
total.failed.push(...group.map((f) => ({ filename: f.name, error: reasonOf(err) })));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return total;
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
onSuccess: (r, files) => {
|
|
96
|
+
setOutcome(r);
|
|
97
|
+
const failedNames = new Set(r.failed.map((f) => f.filename));
|
|
98
|
+
setPicked(files.filter((f) => failedNames.has(f.name)));
|
|
99
|
+
if (input.current) input.current.value = "";
|
|
100
|
+
onUploaded?.();
|
|
101
|
+
},
|
|
102
|
+
invalidates: [["documents"]],
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const finished = outcome !== null && outcome.failed.length === 0;
|
|
107
|
+
return (
|
|
108
|
+
<form className="grid gap-3" onSubmit={(e) => { e.preventDefault(); if (picked.length > 0) void send.mutate(picked); }}>
|
|
109
|
+
<div>
|
|
110
|
+
<Label htmlFor="g8-du-folder" className="mb-1">Add to folder</Label>
|
|
111
|
+
<select id="g8-du-folder" className={SELECT} value={folder} onChange={(e) => setFolder(e.target.value)}>
|
|
112
|
+
{choices.map((p) => <option key={p} value={p}>{folderLabel(p)}</option>)}
|
|
113
|
+
</select>
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<Label htmlFor="g8-du-files" className="mb-1">Files</Label>
|
|
117
|
+
<input ref={input} id="g8-du-files" type="file" multiple accept={LIBRARY_EXTENSIONS.map((e) => `.${e}`).join(",")} onChange={(e) => choose(e.target.files)} className="border-input bg-background w-full rounded-md border p-2 text-sm" />
|
|
118
|
+
<p className="text-muted-foreground mt-1 text-xs">{LIBRARY_EXTENSIONS.join(", ")}. Up to {fmtSize(fileLimit(maxBytes))} each. The server takes {fmtSize(maxBytes)} per request, so several files are sent in several requests. {MAX_BATCH} files at a time.</p>
|
|
119
|
+
</div>
|
|
120
|
+
{notes.length > 0 ? <ul role="alert" className="text-error list-disc pl-5 text-xs">{notes.map((n) => <li key={n}>{n}</li>)}</ul> : null}
|
|
121
|
+
{picked.length > 0 ? (
|
|
122
|
+
<ul aria-label="Files to add" className="divide-y rounded-md border text-sm">
|
|
123
|
+
{picked.map((f) => <li key={f.name} className="flex justify-between gap-2 p-2"><span className="truncate">{f.name}</span><span className="text-muted-foreground shrink-0">{fmtSize(f.size)}</span></li>)}
|
|
124
|
+
</ul>
|
|
125
|
+
) : null}
|
|
126
|
+
{outcome ? (
|
|
127
|
+
<div role="status" className="grid gap-1 rounded-md border p-3 text-sm">
|
|
128
|
+
<p className="font-medium">{outcome.failed.length === 0 ? `${plural(outcome.added, "file")} added.` : `${outcome.added} added, ${outcome.failed.length} not added.`}</p>
|
|
129
|
+
{outcome.failed.map((f) => <p key={f.filename} className="text-error text-xs">{f.filename}: {f.error}</p>)}
|
|
130
|
+
{outcome.warnings.map((w) => <p key={w} className="text-muted-foreground text-xs">{w}</p>)}
|
|
131
|
+
{outcome.failed.length > 0 ? <p className="text-muted-foreground text-xs">Only the files that failed are still selected, so adding again will not duplicate the others.</p> : null}
|
|
132
|
+
</div>
|
|
133
|
+
) : null}
|
|
134
|
+
<div className="flex gap-2">
|
|
135
|
+
{finished ? <Button type="button" onClick={onDone}>Done</Button> : <Button type="submit" loading={send.loading} disabled={picked.length === 0}>{picked.length > 1 ? `Add ${picked.length} files` : "Add file"}</Button>}
|
|
136
|
+
{!finished && onDone ? <Button type="button" variant="outline" onClick={onDone}>Close</Button> : null}
|
|
137
|
+
</div>
|
|
138
|
+
</form>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** One request: a lone file through the single route, several through the batch route (partial success is normal there). */
|
|
143
|
+
async function sendGroup(files: File[], folder: string): Promise<Outcome> {
|
|
144
|
+
const form = new FormData();
|
|
145
|
+
form.append("folder_path", folder);
|
|
146
|
+
if (files.length === 1) {
|
|
147
|
+
form.append("file", files[0]);
|
|
148
|
+
await g8Post("/library/documents/upload", form);
|
|
149
|
+
return { added: 1, failed: [], warnings: [] };
|
|
150
|
+
}
|
|
151
|
+
for (const f of files) form.append("files", f);
|
|
152
|
+
const data = (await g8Post<Rec>("/library/documents/upload-batch", form)).data ?? {};
|
|
153
|
+
const ok = Array.isArray(data.success) ? (data.success as Rec[]) : [];
|
|
154
|
+
const bad = Array.isArray(data.failed) ? (data.failed as Rec[]) : [];
|
|
155
|
+
return {
|
|
156
|
+
added: ok.length,
|
|
157
|
+
failed: bad.map((b) => ({ filename: String(b.filename ?? "A file"), error: String(b.error ?? "It could not be added.") })),
|
|
158
|
+
warnings: ok.filter((o) => o.warning).map((o) => `${String(o.filename ?? "A file")}: ${String(o.warning)}`),
|
|
159
|
+
};
|
|
160
|
+
}
|