@graph8/sdk 0.303.0 → 0.357.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +2220 -23
- package/dist/index.d.ts +2220 -23
- package/dist/index.js +64 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +2082 -20
- package/dist/react.d.ts +2082 -20
- package/dist/react.js +63 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +63 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +7 -3
- package/docs/api/appObjects.d.ts +3 -1
- package/docs/api/appObjects.md +2 -1
- package/docs/api/by-path.md +51 -0
- package/docs/api/dataPipelines.d.ts +149 -0
- package/docs/api/dataPipelines.md +232 -0
- package/docs/api/forms.d.ts +16 -0
- package/docs/api/forms.md +8 -0
- package/docs/api/objects.d.ts +705 -5
- package/docs/api/objects.md +1099 -14
- package/docs/api/ops.d.ts +1 -1
- package/docs/api/ops.md +1 -1
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/quotes.d.ts +3 -1
- package/docs/api/quotes.md +7 -2
- package/docs/api/radar.d.ts +4 -0
- package/docs/api/radar.md +1 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ObjectTable: records of ANY object (custom or standard) as a table, with create, edit and archive.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getObject + listObjectAttributes (via useObjectSchema) read (columns come from the schema)
|
|
8
|
+
* 2. listObjectRecords (GET /objects/{object_slug}/records, cursor paged) read
|
|
9
|
+
* 3. createObjectRecord / updateObjectRecord (RecordFormDialog) write
|
|
10
|
+
* 4. archiveObjectRecord (DELETE /objects/{object_slug}/records/{id}) destructive -> ConfirmSpendDialog
|
|
11
|
+
* The list endpoint has no server-side search, so the search box filters the rows loaded so far;
|
|
12
|
+
* "Load more" fetches the next cursor page. Archived records are excluded by the API.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
|
+
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
20
|
+
import { g8Delete, g8Get } from "@/g8/lib/g8-client";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { usePaginated } from "@/g8/hooks/use-paginated";
|
|
23
|
+
import { useObjectSchema } from "../hooks/use-object-schema";
|
|
24
|
+
import { formatValue, isObjectsApiMissing, recordTitle } from "../lib/object-types";
|
|
25
|
+
import { ObjectNotFound } from "./object-not-found";
|
|
26
|
+
import type { ObjectRecord } from "../lib/object-types";
|
|
27
|
+
import { RecordFormDialog } from "./record-form-dialog";
|
|
28
|
+
|
|
29
|
+
export interface ObjectTableProps {
|
|
30
|
+
objectSlug: string;
|
|
31
|
+
/** Rows per request, 1-200. Default 50. */
|
|
32
|
+
pageSize?: number;
|
|
33
|
+
/** Called when a row is clicked, e.g. to open ObjectRecordDetail. */
|
|
34
|
+
onSelectRecord?: (record: ObjectRecord) => void;
|
|
35
|
+
/** Max columns shown (the first fields in display order). Default 6. */
|
|
36
|
+
maxColumns?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ObjectTable({ objectSlug, pageSize = 50, onSelectRecord, maxColumns = 6 }: ObjectTableProps) {
|
|
40
|
+
const schema = useObjectSchema(objectSlug);
|
|
41
|
+
const slug = encodeURIComponent(objectSlug);
|
|
42
|
+
const records = usePaginated<ObjectRecord>(
|
|
43
|
+
(cursor, signal) => g8Get<ObjectRecord[]>(`/objects/${slug}/records`, { limit: pageSize, cursor }, { signal }),
|
|
44
|
+
[objectSlug, pageSize],
|
|
45
|
+
);
|
|
46
|
+
const [search, setSearch] = useState("");
|
|
47
|
+
const [form, setForm] = useState<{ record?: ObjectRecord } | null>(null);
|
|
48
|
+
const [toArchive, setToArchive] = useState<ObjectRecord | null>(null);
|
|
49
|
+
// Everything below belongs to ONE object: another object must not inherit its search, open dialog or pending archive.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setSearch("");
|
|
52
|
+
setForm(null);
|
|
53
|
+
setToArchive(null);
|
|
54
|
+
}, [objectSlug]);
|
|
55
|
+
const archive = useG8Mutation((record: ObjectRecord) => g8Delete(`/objects/${slug}/records/${encodeURIComponent(record.id)}`), {
|
|
56
|
+
onSuccess: () => { setToArchive(null); void records.reload(); },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const noun = schema.definition?.singular_noun ?? "record";
|
|
60
|
+
const columns = schema.attributes.slice(0, maxColumns);
|
|
61
|
+
const needle = search.trim().toLowerCase();
|
|
62
|
+
const rows = needle
|
|
63
|
+
? records.items.filter((r) => Object.values(r.values ?? {}).some((v) => formatValue(undefined, v).toLowerCase().includes(needle)))
|
|
64
|
+
: records.items;
|
|
65
|
+
|
|
66
|
+
if (isObjectsApiMissing(records.error) || isObjectsApiMissing(schema.error)) return <ObjectNotFound what="object" error={records.error ?? schema.error} />;
|
|
67
|
+
if (schema.error || records.error) {
|
|
68
|
+
return <ApiError error={schema.error ?? records.error} title="Could not load records" onRetry={() => { void schema.refetch(); void records.reload(); }} />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="grid gap-3">
|
|
73
|
+
<div className="flex items-center justify-between gap-2">
|
|
74
|
+
<Input aria-label={`Search loaded ${schema.definition?.plural_noun ?? "records"}`} placeholder="Search loaded rows" className="max-w-xs" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
75
|
+
<Button onClick={() => setForm({})} disabled={schema.loading}>New {noun}</Button>
|
|
76
|
+
</div>
|
|
77
|
+
{records.loading || schema.loading ? (
|
|
78
|
+
<div className="grid gap-2" aria-busy="true">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-9 w-full" />)}</div>
|
|
79
|
+
) : rows.length === 0 ? (
|
|
80
|
+
<EmptyState
|
|
81
|
+
title={needle ? "No loaded rows match" : `No ${schema.definition?.plural_noun?.toLowerCase() ?? "records"} yet`}
|
|
82
|
+
description={needle ? "Clear the search, or load more rows." : `Create the first ${noun} to see it here.`}
|
|
83
|
+
action={needle ? undefined : <Button onClick={() => setForm({})}>New {noun}</Button>}
|
|
84
|
+
/>
|
|
85
|
+
) : (
|
|
86
|
+
<Table>
|
|
87
|
+
<TableHeader>
|
|
88
|
+
<TableRow>
|
|
89
|
+
{columns.map((c) => <TableHead key={c.slug}>{c.title}</TableHead>)}
|
|
90
|
+
<TableHead className="w-32" />
|
|
91
|
+
</TableRow>
|
|
92
|
+
</TableHeader>
|
|
93
|
+
<TableBody>
|
|
94
|
+
{rows.map((record) => (
|
|
95
|
+
<TableRow key={record.id} className={onSelectRecord ? "cursor-pointer" : undefined} onClick={() => onSelectRecord?.(record)}>
|
|
96
|
+
{columns.map((c, i) => (
|
|
97
|
+
<TableCell key={c.slug}>{formatValue(c, record.values?.[c.slug]) || (i === 0 ? recordTitle(schema.definition, schema.attributes, record) : "")}</TableCell>
|
|
98
|
+
))}
|
|
99
|
+
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
|
|
100
|
+
<Button variant="ghost" size="sm" onClick={() => setForm({ record })}>Edit</Button>
|
|
101
|
+
<Button variant="ghost" size="sm" onClick={() => { archive.reset(); setToArchive(record); }}>Archive</Button>
|
|
102
|
+
</TableCell>
|
|
103
|
+
</TableRow>
|
|
104
|
+
))}
|
|
105
|
+
</TableBody>
|
|
106
|
+
</Table>
|
|
107
|
+
)}
|
|
108
|
+
{records.hasMore ? <div><Button variant="outline" onClick={() => void records.loadMore()} loading={records.loadingMore}>Load more</Button></div> : null}
|
|
109
|
+
<RecordFormDialog
|
|
110
|
+
open={form !== null}
|
|
111
|
+
objectSlug={objectSlug}
|
|
112
|
+
singularNoun={noun}
|
|
113
|
+
attributes={schema.attributes}
|
|
114
|
+
record={form?.record}
|
|
115
|
+
onClose={() => setForm(null)}
|
|
116
|
+
onSaved={() => { setForm(null); void records.reload(); }}
|
|
117
|
+
/>
|
|
118
|
+
<ConfirmSpendDialog
|
|
119
|
+
open={toArchive !== null}
|
|
120
|
+
tier="destructive"
|
|
121
|
+
title={`Archive this ${noun}?`}
|
|
122
|
+
description={<><p>It leaves listings but stays readable by id, and its history is kept.</p>{archive.error ? <ApiError error={archive.error} title="Could not archive the record" /> : null}</>}
|
|
123
|
+
confirmLabel="Archive"
|
|
124
|
+
confirming={archive.loading}
|
|
125
|
+
onCancel={() => { archive.reset(); setToArchive(null); }}
|
|
126
|
+
onConfirm={() => { if (toArchive) void archive.mutate(toArchive); }}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordFormDialog: create or edit one record of any object.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. createObjectRecord (POST /objects/{object_slug}/records) write (new record)
|
|
8
|
+
* 2. updateObjectRecord (PATCH /objects/{object_slug}/records/{id}) write (edit; sends expected_revision)
|
|
9
|
+
* Neither is billable, external or destructive, so no confirm dialog. Unknown or invalid values come back
|
|
10
|
+
* as a 422 listing every problem at once; they are shown inline. A 409 means a unique value collided or the
|
|
11
|
+
* record changed since it was loaded (reload and retry).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
18
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
19
|
+
import { buildValues, fromInputValue, toInputValue } from "../lib/object-types";
|
|
20
|
+
import type { ObjectAttribute, ObjectRecord } from "../lib/object-types";
|
|
21
|
+
import { AttributeFieldInput } from "./attribute-field-input";
|
|
22
|
+
|
|
23
|
+
export interface RecordFormDialogProps {
|
|
24
|
+
open: boolean;
|
|
25
|
+
objectSlug: string;
|
|
26
|
+
/** Singular label for the title, e.g. "Invoice". */
|
|
27
|
+
singularNoun?: string;
|
|
28
|
+
attributes: readonly ObjectAttribute[];
|
|
29
|
+
/** Edit this record; omit to create a new one. */
|
|
30
|
+
record?: ObjectRecord;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onSaved: (record: ObjectRecord) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function initialDrafts(attributes: readonly ObjectAttribute[], record?: ObjectRecord): Record<string, string | boolean> {
|
|
36
|
+
const drafts: Record<string, string | boolean> = {};
|
|
37
|
+
for (const attribute of attributes) {
|
|
38
|
+
if (attribute.is_system) continue;
|
|
39
|
+
const stored = record?.values?.[attribute.slug];
|
|
40
|
+
drafts[attribute.slug] = attribute.attribute_type === "checkbox" ? stored === true : toInputValue(attribute, stored);
|
|
41
|
+
}
|
|
42
|
+
return drafts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function RecordFormDialog({ open, objectSlug, singularNoun = "record", attributes, record, onClose, onSaved }: RecordFormDialogProps) {
|
|
46
|
+
const editable = attributes.filter((a) => !a.is_system && !a.is_archived && a.attribute_type !== "formula");
|
|
47
|
+
const [drafts, setDrafts] = useState<Record<string, string | boolean>>(() => initialDrafts(editable, record));
|
|
48
|
+
const slug = encodeURIComponent(objectSlug);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (open) setDrafts(initialDrafts(attributes.filter((a) => !a.is_system && !a.is_archived && a.attribute_type !== "formula"), record));
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [open, record?.id, record?.revision]);
|
|
54
|
+
|
|
55
|
+
const save = useG8Mutation(async () => {
|
|
56
|
+
if (record) {
|
|
57
|
+
// Send only what changed: an untouched empty field would be an explicit null, which clears it (and is a 422 for required ones).
|
|
58
|
+
const before = initialDrafts(editable, record);
|
|
59
|
+
const values: Record<string, unknown> = {};
|
|
60
|
+
for (const attribute of editable) {
|
|
61
|
+
if (drafts[attribute.slug] !== before[attribute.slug]) values[attribute.slug] = fromInputValue(attribute, drafts[attribute.slug] ?? "");
|
|
62
|
+
}
|
|
63
|
+
const res = await g8Patch<ObjectRecord>(`/objects/${slug}/records/${encodeURIComponent(record.id)}`, { values, expected_revision: record.revision });
|
|
64
|
+
return res.data;
|
|
65
|
+
}
|
|
66
|
+
const res = await g8Post<ObjectRecord>(`/objects/${slug}/records`, { values: buildValues(editable, drafts) });
|
|
67
|
+
return res.data;
|
|
68
|
+
}, { onSuccess: (saved) => onSaved(saved) });
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Dialog open={open} onOpenChange={(next) => (next ? undefined : onClose())}>
|
|
72
|
+
<DialogContent className="max-h-[85vh] overflow-y-auto">
|
|
73
|
+
<DialogHeader>
|
|
74
|
+
<DialogTitle>{record ? `Edit ${singularNoun}` : `New ${singularNoun}`}</DialogTitle>
|
|
75
|
+
<DialogDescription>Required fields are marked with an asterisk.</DialogDescription>
|
|
76
|
+
</DialogHeader>
|
|
77
|
+
<form
|
|
78
|
+
className="grid gap-3"
|
|
79
|
+
onSubmit={(e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
void save.mutate(undefined);
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{editable.map((attribute) => (
|
|
85
|
+
<div key={attribute.slug}>
|
|
86
|
+
<Label htmlFor={`g8-attr-${attribute.slug}`} className="mb-1">
|
|
87
|
+
{attribute.title}
|
|
88
|
+
{attribute.is_required ? " *" : ""}
|
|
89
|
+
</Label>
|
|
90
|
+
<AttributeFieldInput
|
|
91
|
+
attribute={attribute}
|
|
92
|
+
value={drafts[attribute.slug] ?? ""}
|
|
93
|
+
disabled={save.loading}
|
|
94
|
+
onChange={(value) => setDrafts((prev) => ({ ...prev, [attribute.slug]: value }))}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
{editable.length === 0 ? <p className="text-muted-foreground text-sm">This object has no fields yet. Add fields in the schema editor first.</p> : null}
|
|
99
|
+
{save.error ? <ApiError error={save.error} title="Could not save the record" /> : null}
|
|
100
|
+
<DialogFooter>
|
|
101
|
+
<Button type="button" variant="outline" onClick={onClose} disabled={save.loading}>
|
|
102
|
+
Cancel
|
|
103
|
+
</Button>
|
|
104
|
+
<Button type="submit" loading={save.loading} disabled={editable.length === 0}>
|
|
105
|
+
{record ? "Save changes" : `Create ${singularNoun}`}
|
|
106
|
+
</Button>
|
|
107
|
+
</DialogFooter>
|
|
108
|
+
</form>
|
|
109
|
+
</DialogContent>
|
|
110
|
+
</Dialog>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RecordRolesPanel: who owns a contact, company or deal (SDR, AE, CSM role slots).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order:
|
|
7
|
+
* 1. getRecordRoleSlots (GET /records/{entity_type}/{entity_id}/role-slots) read
|
|
8
|
+
* 2. assignRecordRole (POST /records/{entity_type}/{entity_id}/role-assignments) write
|
|
9
|
+
* 3. clearRecordRole (DELETE /records/{entity_type}/{entity_id}/role-assignments/{role_slot}) destructive -> ConfirmSpendDialog
|
|
10
|
+
* Only the standard objects (contacts, companies, deals) have role slots; custom objects do not.
|
|
11
|
+
* The API takes a user id, not a name: pass `userOptions` to give people a picker.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
|
|
22
|
+
type Slot = "sdr" | "ae" | "csm";
|
|
23
|
+
const SLOTS: ReadonlyArray<{ slot: Slot; label: string }> = [
|
|
24
|
+
{ slot: "sdr", label: "SDR" },
|
|
25
|
+
{ slot: "ae", label: "Account executive" },
|
|
26
|
+
{ slot: "csm", label: "Customer success" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
interface SlotInfo { user_id?: string | null; user_name?: string | null }
|
|
30
|
+
interface RoleSlots { sdr?: SlotInfo | null; ae?: SlotInfo | null; csm?: SlotInfo | null; can_modify?: boolean }
|
|
31
|
+
|
|
32
|
+
export interface RecordRolesPanelProps {
|
|
33
|
+
entityType: "contact" | "company" | "deal";
|
|
34
|
+
entityId: string;
|
|
35
|
+
/** Optional people to pick from. Without it the user id is typed. */
|
|
36
|
+
userOptions?: ReadonlyArray<{ id: string; name: string }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function RecordRolesPanel({ entityType, entityId, userOptions }: RecordRolesPanelProps) {
|
|
40
|
+
const base = `/records/${entityType}/${encodeURIComponent(entityId)}`;
|
|
41
|
+
const slots = useG8Query((signal) => g8Get<RoleSlots>(`${base}/role-slots`, {}, { signal }).then((r) => r.data), [base]);
|
|
42
|
+
const [draft, setDraft] = useState<Record<string, string>>({});
|
|
43
|
+
const [clearing, setClearing] = useState<Slot | null>(null);
|
|
44
|
+
const [busySlot, setBusySlot] = useState<Slot | null>(null);
|
|
45
|
+
const assign = useG8Mutation((vars: { slot: Slot; userId: string }) => g8Post(`${base}/role-assignments`, { role_slot: vars.slot, user_id: vars.userId }), {
|
|
46
|
+
onSuccess: (_r, vars) => { setDraft((d) => ({ ...d, [vars.slot]: "" })); void slots.refetch(); },
|
|
47
|
+
});
|
|
48
|
+
const assignSlot = async (slot: Slot, userId: string): Promise<void> => {
|
|
49
|
+
setBusySlot(slot);
|
|
50
|
+
await assign.mutate({ slot, userId });
|
|
51
|
+
setBusySlot(null);
|
|
52
|
+
};
|
|
53
|
+
// Typed ids and open dialogs belong to ONE record.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setDraft({});
|
|
56
|
+
setClearing(null);
|
|
57
|
+
assign.reset();
|
|
58
|
+
clear.reset();
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
}, [base]);
|
|
61
|
+
const clear = useG8Mutation((slot: Slot) => g8Delete(`${base}/role-assignments/${slot}`), { onSuccess: () => { setClearing(null); void slots.refetch(); } });
|
|
62
|
+
|
|
63
|
+
if (slots.error) return <ApiError error={slots.error} title="Could not load owners" onRetry={() => void slots.refetch()} />;
|
|
64
|
+
// Write controls only after the API has said the caller may modify: never while loading (they would flash for read-only users).
|
|
65
|
+
const canModify = slots.data != null && slots.data.can_modify !== false;
|
|
66
|
+
return (
|
|
67
|
+
<section aria-label="Owners" className="grid gap-2">
|
|
68
|
+
<h3 className="text-sm font-semibold">Owners</h3>
|
|
69
|
+
{slots.loading && !slots.data ? <p className="text-muted-foreground text-sm">Loading owners...</p> : null}
|
|
70
|
+
{SLOTS.map(({ slot, label }) => {
|
|
71
|
+
const holder = slots.data?.[slot];
|
|
72
|
+
return (
|
|
73
|
+
<div key={slot} className="flex items-center gap-2 text-sm">
|
|
74
|
+
<span className="w-40 shrink-0">{label}</span>
|
|
75
|
+
<span className="flex-1">{holder?.user_name ?? holder?.user_id ?? <span className="text-muted-foreground">Unassigned</span>}</span>
|
|
76
|
+
{canModify ? (
|
|
77
|
+
<>
|
|
78
|
+
{userOptions ? (
|
|
79
|
+
<select aria-label={`${label} user`} value={draft[slot] ?? ""} onChange={(e) => setDraft((d) => ({ ...d, [slot]: e.target.value }))} className="border-input bg-background h-8 rounded-md border px-2 text-sm">
|
|
80
|
+
<option value="">Choose...</option>
|
|
81
|
+
{userOptions.map((u) => <option key={u.id} value={u.id}>{u.name}</option>)}
|
|
82
|
+
</select>
|
|
83
|
+
) : (
|
|
84
|
+
<Input aria-label={`${label} user id`} placeholder="User id" className="h-8 w-40" value={draft[slot] ?? ""} onChange={(e) => setDraft((d) => ({ ...d, [slot]: e.target.value }))} />
|
|
85
|
+
)}
|
|
86
|
+
<Button size="sm" variant="outline" loading={busySlot === slot} disabled={!draft[slot] || busySlot !== null} onClick={() => void assignSlot(slot, draft[slot])}>Assign</Button>
|
|
87
|
+
{holder ? <Button size="sm" variant="ghost" onClick={() => { clear.reset(); setClearing(slot); }}>Clear</Button> : null}
|
|
88
|
+
</>
|
|
89
|
+
) : null}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
{assign.error ? <ApiError error={assign.error} title="Could not assign the owner" /> : null}
|
|
94
|
+
<ConfirmSpendDialog
|
|
95
|
+
open={clearing !== null}
|
|
96
|
+
tier="destructive"
|
|
97
|
+
title="Remove this owner?"
|
|
98
|
+
description={<><p>The slot becomes unassigned. The change is kept in the role history.</p>{clear.error ? <ApiError error={clear.error} title="Could not clear the owner" /> : null}</>}
|
|
99
|
+
confirmLabel="Remove"
|
|
100
|
+
confirming={clear.loading}
|
|
101
|
+
onCancel={() => { clear.reset(); setClearing(null); }}
|
|
102
|
+
onConfirm={() => { if (clearing) void clear.mutate(clearing); }}
|
|
103
|
+
/>
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
4
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
5
|
+
import { orderedAttributes } from "../lib/object-types";
|
|
6
|
+
import type { ObjectAttribute, ObjectDefinition } from "../lib/object-types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load one object's definition and its active attributes.
|
|
10
|
+
*
|
|
11
|
+
* Operations in order: getObject (GET /objects/{object_slug}), listObjectAttributes
|
|
12
|
+
* (GET /objects/{object_slug}/attributes). Both are `read`.
|
|
13
|
+
*/
|
|
14
|
+
export function useObjectSchema(objectSlug: string | undefined) {
|
|
15
|
+
const slug = objectSlug ? encodeURIComponent(objectSlug) : "";
|
|
16
|
+
const definition = useG8Query(
|
|
17
|
+
(signal) => g8Get<ObjectDefinition>(`/objects/${slug}`, {}, { signal }).then((r) => r.data),
|
|
18
|
+
[slug],
|
|
19
|
+
{ enabled: Boolean(objectSlug) },
|
|
20
|
+
);
|
|
21
|
+
const attributes = useG8Query(
|
|
22
|
+
(signal) => g8Get<ObjectAttribute[]>(`/objects/${slug}/attributes`, {}, { signal }).then((r) => r.data ?? []),
|
|
23
|
+
[slug],
|
|
24
|
+
{ enabled: Boolean(objectSlug) },
|
|
25
|
+
);
|
|
26
|
+
const refetch = async (): Promise<void> => {
|
|
27
|
+
await Promise.all([definition.refetch(), attributes.refetch()]);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
definition: definition.data,
|
|
31
|
+
attributes: orderedAttributes(attributes.data ?? []),
|
|
32
|
+
loading: definition.loading || attributes.loading,
|
|
33
|
+
error: definition.error ?? attributes.error,
|
|
34
|
+
refetch,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Types and pure helpers shared by the objects block.
|
|
5
|
+
*
|
|
6
|
+
* Object records are untyped by design: the valid keys of `record.values` are the
|
|
7
|
+
* attribute slugs of YOUR object, which you only know at runtime. These helpers turn
|
|
8
|
+
* an attribute definition into the right input, display text and API value.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { G8Error } from "@graph8/sdk";
|
|
12
|
+
|
|
13
|
+
export type AttributeType =
|
|
14
|
+
| "text" | "number" | "currency" | "date" | "timestamp" | "checkbox" | "select" | "status"
|
|
15
|
+
| "email_address" | "phone_number" | "domain" | "location" | "personal_name" | "record_reference"
|
|
16
|
+
| "rating" | "actor_reference";
|
|
17
|
+
|
|
18
|
+
/** Types you can create from the editor. `formula` exists in the API but needs an expression tree, so it is left to recipes. */
|
|
19
|
+
export const ATTRIBUTE_TYPES: ReadonlyArray<{ value: AttributeType; label: string }> = [
|
|
20
|
+
{ value: "text", label: "Text" },
|
|
21
|
+
{ value: "number", label: "Number" },
|
|
22
|
+
{ value: "currency", label: "Currency" },
|
|
23
|
+
{ value: "date", label: "Date" },
|
|
24
|
+
{ value: "timestamp", label: "Date and time" },
|
|
25
|
+
{ value: "checkbox", label: "Checkbox" },
|
|
26
|
+
{ value: "select", label: "Select" },
|
|
27
|
+
{ value: "status", label: "Status" },
|
|
28
|
+
{ value: "email_address", label: "Email" },
|
|
29
|
+
{ value: "phone_number", label: "Phone" },
|
|
30
|
+
{ value: "domain", label: "Domain" },
|
|
31
|
+
{ value: "location", label: "Location" },
|
|
32
|
+
{ value: "personal_name", label: "Person name" },
|
|
33
|
+
{ value: "record_reference", label: "Reference to a record" },
|
|
34
|
+
{ value: "rating", label: "Rating" },
|
|
35
|
+
{ value: "actor_reference", label: "Team member" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const OBJECT_ICONS = ["box", "briefcase", "building", "calendar", "flag", "folder", "globe", "heart", "layers", "package", "target", "users"] as const;
|
|
39
|
+
export type ObjectIcon = (typeof OBJECT_ICONS)[number];
|
|
40
|
+
|
|
41
|
+
export interface ObjectDefinition {
|
|
42
|
+
slug: string;
|
|
43
|
+
singular_noun: string;
|
|
44
|
+
plural_noun: string;
|
|
45
|
+
description?: string | null;
|
|
46
|
+
icon?: string | null;
|
|
47
|
+
display_attribute_slug?: string | null;
|
|
48
|
+
is_archived?: boolean;
|
|
49
|
+
is_system?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ObjectAttribute {
|
|
53
|
+
slug: string;
|
|
54
|
+
title: string;
|
|
55
|
+
attribute_type: string;
|
|
56
|
+
description?: string | null;
|
|
57
|
+
config?: Record<string, unknown>;
|
|
58
|
+
is_required?: boolean;
|
|
59
|
+
is_unique?: boolean;
|
|
60
|
+
is_multiselect?: boolean;
|
|
61
|
+
is_archived?: boolean;
|
|
62
|
+
is_system?: boolean;
|
|
63
|
+
sort_order?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ObjectRecord {
|
|
67
|
+
id: string;
|
|
68
|
+
object_slug: string;
|
|
69
|
+
values: Record<string, unknown>;
|
|
70
|
+
revision?: number;
|
|
71
|
+
is_archived?: boolean;
|
|
72
|
+
created_at?: string | null;
|
|
73
|
+
updated_at?: string | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Slugs are lowercase snake_case, start with a letter, max 63 characters. */
|
|
77
|
+
export function slugify(text: string): string {
|
|
78
|
+
const slug = text
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
81
|
+
.replace(/^_+|_+$/g, "")
|
|
82
|
+
.slice(0, 63);
|
|
83
|
+
return /^[a-z]/.test(slug) ? slug : slug ? `f_${slug}`.slice(0, 63) : "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isValidSlug(slug: string): boolean {
|
|
87
|
+
return /^[a-z][a-z0-9_]{0,62}$/.test(slug);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Active attributes in display order (sort_order, then slug). */
|
|
91
|
+
export function orderedAttributes(attributes: readonly ObjectAttribute[]): ObjectAttribute[] {
|
|
92
|
+
return attributes
|
|
93
|
+
.filter((a) => !a.is_archived)
|
|
94
|
+
.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || a.slug.localeCompare(b.slug));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The allowed values of a select/status attribute. Options may be strings or {value|id|slug|label} objects. */
|
|
98
|
+
export function selectOptions(attribute: ObjectAttribute): string[] {
|
|
99
|
+
const raw = attribute.config?.options;
|
|
100
|
+
if (!Array.isArray(raw)) return [];
|
|
101
|
+
return raw
|
|
102
|
+
.map((o) => (typeof o === "string" ? o : o && typeof o === "object" ? String((o as Record<string, unknown>).value ?? (o as Record<string, unknown>).id ?? (o as Record<string, unknown>).slug ?? (o as Record<string, unknown>).label ?? "") : ""))
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scalarText(value: unknown): string {
|
|
107
|
+
if (value == null) return "";
|
|
108
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
109
|
+
if (typeof value === "object") {
|
|
110
|
+
const o = value as Record<string, unknown>;
|
|
111
|
+
const named = o.full_name ?? o.name ?? o.label ?? o.title ?? o.record_id ?? o.id ?? o.actor_id;
|
|
112
|
+
return named != null ? String(named) : JSON.stringify(value);
|
|
113
|
+
}
|
|
114
|
+
return String(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Human text for a stored value. Lists are comma separated, booleans Yes/No, objects use their name. */
|
|
118
|
+
export function formatValue(attribute: ObjectAttribute | undefined, value: unknown): string {
|
|
119
|
+
if (value == null || value === "") return "";
|
|
120
|
+
if (Array.isArray(value)) return value.map(scalarText).join(", ");
|
|
121
|
+
if (attribute?.attribute_type === "currency" && typeof value === "number") {
|
|
122
|
+
const code = typeof attribute.config?.currency_code === "string" ? attribute.config.currency_code : "USD";
|
|
123
|
+
try {
|
|
124
|
+
return new Intl.NumberFormat(undefined, { style: "currency", currency: code }).format(value);
|
|
125
|
+
} catch {
|
|
126
|
+
return `${value} ${code}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if ((attribute?.attribute_type === "date" || attribute?.attribute_type === "timestamp") && typeof value === "string") {
|
|
130
|
+
const d = new Date(value);
|
|
131
|
+
if (!Number.isNaN(d.getTime())) return attribute.attribute_type === "date" ? d.toISOString().slice(0, 10) : d.toLocaleString();
|
|
132
|
+
}
|
|
133
|
+
return scalarText(value);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** An ISO instant as the local "YYYY-MM-DDTHH:mm" a datetime-local input shows. */
|
|
137
|
+
export function toLocalDateTimeInput(iso: string): string {
|
|
138
|
+
const d = new Date(iso);
|
|
139
|
+
if (Number.isNaN(d.getTime())) return iso.slice(0, 16);
|
|
140
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
141
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The text shown in an `<input>` for the stored value. */
|
|
145
|
+
export function toInputValue(attribute: ObjectAttribute, value: unknown): string {
|
|
146
|
+
if (value == null) return "";
|
|
147
|
+
if (Array.isArray(value)) return value.map(scalarText).join(", ");
|
|
148
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
149
|
+
if (attribute.attribute_type === "timestamp" && typeof value === "string") return toLocalDateTimeInput(value);
|
|
150
|
+
return String(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Turn what the user typed into the API value. Returns `null` for an empty input
|
|
155
|
+
* (which CLEARS an optional value on update) and a typed value otherwise.
|
|
156
|
+
*/
|
|
157
|
+
export function fromInputValue(attribute: ObjectAttribute, raw: string | boolean): unknown {
|
|
158
|
+
if (attribute.attribute_type === "checkbox") return raw === true || raw === "true";
|
|
159
|
+
const text = String(raw).trim();
|
|
160
|
+
if (text === "") return null;
|
|
161
|
+
if (attribute.is_multiselect) return text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
162
|
+
if (["number", "currency", "rating"].includes(attribute.attribute_type)) return Number(text);
|
|
163
|
+
// A datetime-local value has no zone: read it as the person's local time and send the exact instant.
|
|
164
|
+
if (attribute.attribute_type === "timestamp") {
|
|
165
|
+
const d = new Date(text);
|
|
166
|
+
return Number.isNaN(d.getTime()) ? text : d.toISOString();
|
|
167
|
+
}
|
|
168
|
+
// Date-only values stay "YYYY-MM-DD": never routed through Date, which would shift the day by time zone.
|
|
169
|
+
if ((attribute.attribute_type === "location" || attribute.attribute_type === "personal_name") && text.startsWith("{")) {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(text) as unknown;
|
|
172
|
+
} catch {
|
|
173
|
+
return text;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return text;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Values to send on create: only attributes the user filled in (an unset one would be a null the API may reject). */
|
|
180
|
+
export function buildValues(attributes: readonly ObjectAttribute[], drafts: Record<string, string | boolean>): Record<string, unknown> {
|
|
181
|
+
const out: Record<string, unknown> = {};
|
|
182
|
+
for (const attribute of attributes) {
|
|
183
|
+
const draft = drafts[attribute.slug];
|
|
184
|
+
if (draft === undefined) continue;
|
|
185
|
+
const value = fromInputValue(attribute, draft);
|
|
186
|
+
if (value !== null) out[attribute.slug] = value;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A 404 from the objects API itself (not a single object/record): the backend at this base URL does not serve
|
|
193
|
+
* custom objects (wrong G8_API_URL or an older deployment). A missing scope or a proxy-blocked prefix is a 403.
|
|
194
|
+
*/
|
|
195
|
+
export function isObjectsApiMissing(error: unknown): boolean {
|
|
196
|
+
return error instanceof G8Error && error.status === 404;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @deprecated Renamed to {@link isObjectsApiMissing}; there is no platform gate. Kept so existing imports work. */
|
|
200
|
+
export const isPlatformGated = isObjectsApiMissing;
|
|
201
|
+
|
|
202
|
+
/** The attribute used as a record's title. */
|
|
203
|
+
export function recordTitle(definition: ObjectDefinition | undefined, attributes: readonly ObjectAttribute[], record: ObjectRecord): string {
|
|
204
|
+
const slug = definition?.display_attribute_slug ?? attributes.find((a) => a.attribute_type === "text")?.slug ?? attributes[0]?.slug;
|
|
205
|
+
const text = slug ? formatValue(attributes.find((a) => a.slug === slug), record.values?.[slug]) : "";
|
|
206
|
+
return text || record.id;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The standard objects that support owner roles (SDR/AE/CSM), mapped to the role API's entity type. */
|
|
210
|
+
export const ROLE_ENTITY_TYPES: Record<string, "contact" | "company" | "deal"> = {
|
|
211
|
+
contacts: "contact",
|
|
212
|
+
companies: "company",
|
|
213
|
+
deals: "deal",
|
|
214
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Define a custom object, add its fields, create the first records
|
|
3
|
+
* @when Modelling something graph8 has no table for (an invoice, a shipment, a subscription) before the
|
|
4
|
+
* ObjectTable screen has anything to show. Needs an Admin key with objects:manage and objects:write.
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
// 1. The object. The slug is immutable and becomes the URL segment: /objects/invoices/...
|
|
11
|
+
const invoices = await g8.objects.create({ slug: "invoices", singular_noun: "Invoice", plural_noun: "Invoices", icon: "package" });
|
|
12
|
+
|
|
13
|
+
// 2. Fields, in display order. Slugs are lowercase snake_case. select/status need config.options.
|
|
14
|
+
await g8.objects.createAttribute(invoices.slug, { slug: "number", title: "Number", attribute_type: "text", is_required: true, is_unique: true, sort_order: 1 });
|
|
15
|
+
await g8.objects.createAttribute(invoices.slug, { slug: "amount", title: "Amount", attribute_type: "currency", config: { currency_code: "USD" }, sort_order: 2 });
|
|
16
|
+
await g8.objects.createAttribute(invoices.slug, { slug: "status", title: "Status", attribute_type: "status", config: { options: ["draft", "sent", "paid", "overdue"] }, sort_order: 3 });
|
|
17
|
+
await g8.objects.createAttribute(invoices.slug, { slug: "due", title: "Due date", attribute_type: "date", sort_order: 4 });
|
|
18
|
+
|
|
19
|
+
// 3. Make the first text field the record title in lists and pickers.
|
|
20
|
+
await g8.objects.update(invoices.slug, { display_attribute_slug: "number" });
|
|
21
|
+
|
|
22
|
+
// 4. Records. Unknown fields are a 422 (never silently dropped); a duplicate unique value is a 409.
|
|
23
|
+
const record = await g8.objects.createRecord(invoices.slug, { number: "INV-1001", amount: 1200, status: "sent", due: "2026-11-30" });
|
|
24
|
+
console.log(record.id, record.revision, record.values);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Page through records, update safely, read the history, archive and restore
|
|
3
|
+
* @when Building admin tooling or an agent that edits records. `expectedRevision` makes an update a conditional
|
|
4
|
+
* write so two editors cannot silently overwrite each other (the API answers 409 and you re-read).
|
|
5
|
+
*/
|
|
6
|
+
import { g8, G8Error } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
// 1. Walk every record with the cursor (page numbers also work, the cursor wins when both are sent).
|
|
11
|
+
const all: Array<{ id: string; revision?: number; values: Record<string, unknown> }> = [];
|
|
12
|
+
let cursor: string | undefined;
|
|
13
|
+
do {
|
|
14
|
+
const page = await g8.objects.listRecords("invoices", { limit: 100, cursor });
|
|
15
|
+
all.push(...page.data);
|
|
16
|
+
cursor = page.pagination?.next_cursor ?? undefined;
|
|
17
|
+
} while (cursor);
|
|
18
|
+
|
|
19
|
+
const overdue = all.filter((r) => r.values.status === "sent" && String(r.values.due ?? "") < new Date().toISOString().slice(0, 10));
|
|
20
|
+
|
|
21
|
+
for (const record of overdue) {
|
|
22
|
+
try {
|
|
23
|
+
// 2. Partial update: only `status` changes, other values are left alone. Sending null would CLEAR a value.
|
|
24
|
+
await g8.objects.updateRecord("invoices", record.id, { status: "overdue" }, { expectedRevision: record.revision });
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err instanceof G8Error && err.status === 409) continue; // changed since we read it: skip, a later run picks it up
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. What changed, and who changed it.
|
|
32
|
+
if (overdue[0]) {
|
|
33
|
+
const changes = await g8.objects.changes("invoices", overdue[0].id, { limit: 10 });
|
|
34
|
+
for (const entry of changes.entries) console.log(entry.occurred_at, entry.action, entry.actor_type, Object.keys(entry.changes));
|
|
35
|
+
|
|
36
|
+
// 4. Archive hides the record but keeps it and its history; restore brings it back.
|
|
37
|
+
await g8.objects.archiveRecord("invoices", overdue[0].id);
|
|
38
|
+
await g8.objects.restoreRecord("invoices", overdue[0].id);
|
|
39
|
+
}
|