@graph8/sdk 0.362.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +64 -1
- package/blocks/_foundation/block.json +79 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/screen/index.ts +1 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/block.json +6 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-list.tsx +9 -5
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/block.json +9 -3
- package/blocks/deals/components/deal-detail.tsx +23 -11
- package/blocks/deals/components/deal-related.tsx +1 -1
- package/blocks/deals/components/deals-screen.tsx +10 -5
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/block.json +32 -18
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +5 -2
- package/blocks/forms/components/submissions-table.tsx +3 -1
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/block.json +40 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +10 -6
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/block.json +28 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/quotes/block.json +17 -12
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-related.tsx +5 -2
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +11 -0
- package/blocks/social-ads/block.json +101 -2
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2709 -1721
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +22 -19
- package/dist/types/blocks/doctor.d.ts +5 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +99 -89
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReportDetail: everything about one saved custom-object report: its definition (object, measure, grouping, filters,
|
|
5
|
+
* chart type, visibility, revision, owner dates), the latest result table, and the actions Run again and Delete.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. GET /reports/{id} (reports.getObjectReportApi) read the definition
|
|
9
|
+
* 2. POST /reports/{id}/execute (reports.executeObjectReportApi) write run it (creates a run record, sends nothing); runs once on open
|
|
10
|
+
* 3. DELETE /reports/{id} (reports.archiveObjectReportApi) DESTRUCTIVE -> ConfirmSpendDialog
|
|
11
|
+
* Editing happens in ReportBuilder (`reportId`). Preview and execute share the org's report-run slots (429 `too_many_report_runs`).
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useState } from "react";
|
|
14
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
15
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
16
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
17
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
18
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
19
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
22
|
+
import { reportBody, type ReportResultData, type SavedReport } from "../lib/analytics-types";
|
|
23
|
+
import { ResultTable } from "./report-builder";
|
|
24
|
+
|
|
25
|
+
export interface ReportDetailProps {
|
|
26
|
+
reportId: string;
|
|
27
|
+
/** Called after the report was deleted. */
|
|
28
|
+
onDeleted?: () => void;
|
|
29
|
+
/** A result the host already holds (kept across tab switches). While set, opening does not run the report again. */
|
|
30
|
+
result?: ReportResultData | null;
|
|
31
|
+
/** Called with every fresh result, so the host can keep it. */
|
|
32
|
+
onResult?: (result: ReportResultData | null) => void;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Cfg = { measures?: Array<Record<string, unknown>>; group_by?: Array<Record<string, unknown>>; filters?: unknown[] };
|
|
37
|
+
|
|
38
|
+
const measureText = (m: Record<string, unknown> | undefined): string => (m ? `${String(m.kind)}${m.attribute_slug ? ` of ${String(m.attribute_slug)}` : ""}` : "");
|
|
39
|
+
const groupText = (g: Record<string, unknown> | undefined): string => (g ? (g.kind === "created_at" ? `created date by ${String(g.grain ?? "month")}` : String(g.attribute_slug ?? "")) : "None");
|
|
40
|
+
|
|
41
|
+
export function ReportDetail({ reportId, onDeleted, result: given, onResult, className }: ReportDetailProps) {
|
|
42
|
+
const [confirming, setConfirming] = useState(false);
|
|
43
|
+
const [own, setOwn] = useState<ReportResultData | null>(null);
|
|
44
|
+
const result = given ?? own;
|
|
45
|
+
const setResult = (r: ReportResultData | null) => { setOwn(r); onResult?.(r); };
|
|
46
|
+
const report = useG8Query(async (signal) => reportBody<SavedReport>(await g8Get(`/reports/${encodeURIComponent(reportId)}`, undefined, { signal })), [reportId]);
|
|
47
|
+
const run = useG8Mutation(async () => { const out = reportBody<ReportResultData>(await g8Post(`/reports/${encodeURIComponent(reportId)}/execute`, {})); setResult(out); return out; });
|
|
48
|
+
const remove = useG8Mutation(async () => { await g8Delete(`/reports/${encodeURIComponent(reportId)}`); }, { onSuccess: () => { setConfirming(false); onDeleted?.(); }, onError: () => setConfirming(false) });
|
|
49
|
+
|
|
50
|
+
// A different report starts clean; it runs once on open unless the host already holds a result.
|
|
51
|
+
useEffect(() => { run.reset(); if (!given) void run.mutate(undefined); }, [reportId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
52
|
+
|
|
53
|
+
const r = report.data;
|
|
54
|
+
const cfg = (r?.config ?? {}) as Cfg;
|
|
55
|
+
if (report.error) return <ApiError error={report.error} title="Could not load the report" onRetry={() => void report.refetch()} />;
|
|
56
|
+
if (!r) return <Skeleton className="h-40 w-full" aria-label="Loading report" />;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className={`space-y-4 ${className ?? ""}`} data-testid="report-detail">
|
|
60
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
61
|
+
<StatusPill tone={r.visibility === "private" ? "neutral" : "info"}>{r.visibility}</StatusPill>
|
|
62
|
+
<span className="text-xs text-muted-foreground">Revision {r.revision}</span>
|
|
63
|
+
<div className="ml-auto flex gap-2">
|
|
64
|
+
<Button size="sm" variant="outline" loading={run.loading} onClick={() => void run.mutate(undefined)}>Run again</Button>
|
|
65
|
+
{r.can_delete ? <Button size="sm" variant="ghost" onClick={() => setConfirming(true)}>Delete</Button> : null}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<KeyValueList columns={2} items={[
|
|
69
|
+
{ label: "Object", value: r.source.plural_noun, mono: false },
|
|
70
|
+
{ label: "Object slug", value: r.source.slug, mono: true },
|
|
71
|
+
{ label: "Chart", value: r.chart_type },
|
|
72
|
+
{ label: "Measure", value: (cfg.measures ?? []).map(measureText).join(", ") || "count" },
|
|
73
|
+
{ label: "Grouped by", value: (cfg.group_by ?? []).map(groupText).join(", ") || "None" },
|
|
74
|
+
{ label: "Filters", value: (cfg.filters ?? []).length ? `${(cfg.filters ?? []).length} applied` : "None" },
|
|
75
|
+
]} />
|
|
76
|
+
{run.error ? <ApiError error={run.error} title="Could not run the report" onRetry={() => void run.mutate(undefined)} /> : null}
|
|
77
|
+
{run.loading && !result ? <Skeleton className="h-24 w-full" aria-label="Running report" /> : null}
|
|
78
|
+
{result ? <ResultTable result={result} /> : null}
|
|
79
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the report" /> : null}
|
|
80
|
+
<ConfirmSpendDialog open={confirming} tier="destructive" title={`Delete "${r.title}"?`} description="The saved report is removed for everyone who can see it. The underlying records are not touched."
|
|
81
|
+
confirmLabel="Delete report" affected={1} confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setConfirming(false)} />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TrendChart: a daily line chart for one outbound channel (email, calls or meetings), drawn with the kit TimeSeriesChart (recharts).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations (pick the channel):
|
|
7
|
+
* channel "email": GET /analytics/outbound/email-metrics/trend?days= (analytics.getEmailTrend) read rows { date, sent, replied, ... }
|
|
8
|
+
* channel "calls": GET /analytics/outbound/call-metrics/trend?days= (analytics.getCallTrend) read rows { date, dials, connected, ... }
|
|
9
|
+
* channel "meetings": GET /analytics/outbound/meeting-metrics/trend?days= (analytics.getMeetingTrend) read rows { date, booked, cancelled, held, ... }
|
|
10
|
+
* Two series per channel (the volume and the outcome). Dates are plain `YYYY-MM-DD` days and are shown as UTC days.
|
|
11
|
+
*/
|
|
12
|
+
import { Skeleton } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
15
|
+
import { TimeSeriesChart } from "@/g8/components/screen/time-series-chart";
|
|
16
|
+
import { useReport } from "../hooks/use-report";
|
|
17
|
+
import { formatNumber } from "../lib/analytics-types";
|
|
18
|
+
|
|
19
|
+
export type TrendChannel = "email" | "calls" | "meetings";
|
|
20
|
+
|
|
21
|
+
export const TREND_SERIES: Record<TrendChannel, { path: string; keys: [string, string]; labels: [string, string] }> = {
|
|
22
|
+
email: { path: "/analytics/outbound/email-metrics/trend", keys: ["sent", "replied"], labels: ["Sent", "Replied"] },
|
|
23
|
+
calls: { path: "/analytics/outbound/call-metrics/trend", keys: ["dials", "connected"], labels: ["Dials", "Connected"] },
|
|
24
|
+
meetings: { path: "/analytics/outbound/meeting-metrics/trend", keys: ["booked", "held"], labels: ["Booked", "Held"] },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type TrendRow = { date?: string } & Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
/** Daily values of one key, in date order. */
|
|
30
|
+
export function seriesOf(rows: TrendRow[] | undefined, key: string): number[] {
|
|
31
|
+
return (Array.isArray(rows) ? rows : []).map((r) => Number(r[key] ?? 0) || 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TrendChartProps {
|
|
35
|
+
channel?: TrendChannel;
|
|
36
|
+
days?: number;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function TrendChart({ channel = "email", days = 30, className }: TrendChartProps) {
|
|
41
|
+
const spec = TREND_SERIES[channel];
|
|
42
|
+
const { data, loading, error, refetch } = useReport<TrendRow[]>(spec.path, { days });
|
|
43
|
+
const rows = Array.isArray(data) ? data : [];
|
|
44
|
+
const a = seriesOf(rows, spec.keys[0]);
|
|
45
|
+
const b = seriesOf(rows, spec.keys[1]);
|
|
46
|
+
const total = (v: number[]) => v.reduce((x, y) => x + y, 0);
|
|
47
|
+
|
|
48
|
+
if (error) return <ApiError error={error} title="Could not load the trend" onRetry={() => void refetch()} />;
|
|
49
|
+
if (loading && !data) return <Skeleton className="h-48 w-full" aria-label="Loading trend" />;
|
|
50
|
+
if (rows.length === 0 || total(a) === 0) return <EmptyState className={className} title="No activity in this window" description="Daily numbers appear here as emails, calls and meetings are recorded." />;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<figure className={className} aria-label={`${channel} trend`}>
|
|
54
|
+
<div className="mb-2 flex flex-wrap gap-4 text-xs text-muted-foreground">
|
|
55
|
+
<span>{spec.labels[0]} {formatNumber(total(a))}</span>
|
|
56
|
+
<span>{spec.labels[1]} {formatNumber(total(b))}</span>
|
|
57
|
+
</div>
|
|
58
|
+
<TimeSeriesChart data={rows} xKey="date" kind="line" height={200}
|
|
59
|
+
series={[{ key: spec.keys[0], label: spec.labels[0] }, { key: spec.keys[1], label: spec.labels[1], tone: "positive" }]} />
|
|
60
|
+
</figure>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -113,8 +113,12 @@
|
|
|
113
113
|
"name": "MeetingsScreen",
|
|
114
114
|
"file": "components/meetings-screen.tsx",
|
|
115
115
|
"purpose": "Finished meetings product: upcoming, past, cancelled and recorded meetings with detail, approvals, notes and a schedule-a-meeting flow.",
|
|
116
|
-
"operations": [
|
|
117
|
-
|
|
116
|
+
"operations": [
|
|
117
|
+
"list_bookings_appointments_bookings_get"
|
|
118
|
+
],
|
|
119
|
+
"tiers": [
|
|
120
|
+
"read"
|
|
121
|
+
]
|
|
118
122
|
},
|
|
119
123
|
{
|
|
120
124
|
"name": "SchedulingScreen",
|
|
@@ -23,6 +23,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
23
23
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
24
|
import { useSchedules } from "../hooks/use-schedules";
|
|
25
25
|
import { WEEKDAYS, listOf, toHHMM, unwrap, type AvailabilityRule, type Schedule, type TimeWindow } from "../lib/appointments-types";
|
|
26
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
26
27
|
|
|
27
28
|
export interface AvailabilityEditorProps {
|
|
28
29
|
/** Schedule to edit; defaults to the org's default schedule. */
|
|
@@ -72,6 +73,7 @@ function WindowRow({ window: w, onChange, onRemove }: { window: TimeWindow; onCh
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
function CalendarPanel({ connectUrl }: { connectUrl: string }) {
|
|
76
|
+
const brand = useBrand();
|
|
75
77
|
const cals = useG8Query(async (signal) => listOf<CalendarRow>(await g8Get("/appointments/calendars", undefined, { signal }), "calendars"), []);
|
|
76
78
|
return (
|
|
77
79
|
<section aria-label="Calendars" className="space-y-2 rounded-lg border p-3 text-sm">
|
|
@@ -84,22 +86,26 @@ function CalendarPanel({ connectUrl }: { connectUrl: string }) {
|
|
|
84
86
|
))}
|
|
85
87
|
{!cals.loading && !cals.error && (cals.data ?? []).length === 0 ? <p className="text-muted-foreground">No calendar connected, so busy time is not checked.</p> : null}
|
|
86
88
|
<a className="underline" href={connectUrl} target="_blank" rel="noreferrer">
|
|
87
|
-
Connect or manage calendars in
|
|
89
|
+
Connect or manage calendars in {brand.linkLabel}
|
|
88
90
|
</a>
|
|
89
91
|
</section>
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export function AvailabilityEditor({ scheduleId, connectCalendarUrl
|
|
95
|
+
export function AvailabilityEditor({ scheduleId, connectCalendarUrl: connectCalendarUrlProp, onSaved, className }: AvailabilityEditorProps) {
|
|
96
|
+
const brand = useBrand();
|
|
97
|
+
const connectCalendarUrl = connectCalendarUrlProp ?? brand.appUrl;
|
|
94
98
|
const { schedules, loading: loadingSchedules, error: schedulesError } = useSchedules();
|
|
95
99
|
const [picked, setPicked] = useState<number | undefined>(scheduleId);
|
|
96
100
|
const activeId = picked ?? schedules[0]?.id;
|
|
97
101
|
const [week, setWeek] = useState<Week>({});
|
|
98
102
|
const [overrides, setOverrides] = useState<Override[]>([]);
|
|
99
103
|
const [newDate, setNewDate] = useState("");
|
|
104
|
+
/** The schedule whose hours are in the form. Save is offered only for a schedule that has loaded, never over a blank week. */
|
|
105
|
+
const [appliedFor, setAppliedFor] = useState<number | undefined>(undefined);
|
|
100
106
|
|
|
101
107
|
// Stale context: switching schedule must not show (or save!) the previous schedule's hours.
|
|
102
|
-
useEffect(() => { setWeek({}); setOverrides([]); }, [activeId]);
|
|
108
|
+
useEffect(() => { setWeek({}); setOverrides([]); setAppliedFor(undefined); }, [activeId]);
|
|
103
109
|
|
|
104
110
|
const availability = useG8Query(
|
|
105
111
|
async (signal) => unwrap<Schedule>(await g8Get(`/appointments/schedules/${activeId}/availability`, undefined, { signal })),
|
|
@@ -109,9 +115,10 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
109
115
|
useEffect(() => {
|
|
110
116
|
const data = availability.data;
|
|
111
117
|
if (!data) return;
|
|
118
|
+
setAppliedFor(activeId);
|
|
112
119
|
setWeek(toWeek(data.availability ?? []));
|
|
113
120
|
setOverrides((data.overrides ?? []).filter((o) => o.date).map((o) => ({ date: o.date as string, start: toHHMM(o.start_time), end: toHHMM(o.end_time, "17:00") })));
|
|
114
|
-
}, [availability.data]);
|
|
121
|
+
}, [availability.data, activeId]);
|
|
115
122
|
|
|
116
123
|
const save = useG8Mutation(
|
|
117
124
|
async () =>
|
|
@@ -122,10 +129,11 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
122
129
|
{ onSuccess: () => onSaved?.() },
|
|
123
130
|
);
|
|
124
131
|
|
|
132
|
+
const ready = appliedFor === activeId && !availability.error && !availability.loading;
|
|
125
133
|
const setDay = (day: number, windows: TimeWindow[]) => setWeek((w) => ({ ...w, [day]: windows }));
|
|
126
134
|
if (schedulesError) return <ApiError error={schedulesError} title="Could not load schedules" />;
|
|
127
135
|
if (loadingSchedules) return <Skeleton className="h-64 w-full" aria-label="Loading schedules" />;
|
|
128
|
-
if (activeId == null) return <EmptyState title="No schedule yet" description=
|
|
136
|
+
if (activeId == null) return <EmptyState title="No schedule yet" description={`Create a working-hours schedule in ${brand.linkLabel} (or POST /appointments/schedules), then edit it here.`} />;
|
|
129
137
|
|
|
130
138
|
return (
|
|
131
139
|
<div className={`space-y-4 ${className ?? ""}`}>
|
|
@@ -141,6 +149,8 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
141
149
|
<p className="text-sm text-muted-foreground">Times are wall-clock hours in the schedule's time zone: {schedules.find((x) => x.id === activeId)?.time_zone ?? "the schedule's zone"}.</p>
|
|
142
150
|
{availability.error ? <ApiError error={availability.error} title="Could not load availability" onRetry={() => void availability.refetch()} /> : null}
|
|
143
151
|
|
|
152
|
+
{!ready ? (availability.error ? null : <Skeleton className="h-48 w-full" aria-label="Loading availability" />) : (
|
|
153
|
+
<>
|
|
144
154
|
<div className="space-y-2">
|
|
145
155
|
{WEEKDAYS.map(({ day, label }) => {
|
|
146
156
|
const windows = week[day] ?? [];
|
|
@@ -186,6 +196,8 @@ export function AvailabilityEditor({ scheduleId, connectCalendarUrl = "https://a
|
|
|
186
196
|
<Button onClick={() => void save.mutate(undefined)} loading={save.loading}>
|
|
187
197
|
Save availability
|
|
188
198
|
</Button>
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
189
201
|
<CalendarPanel connectUrl={connectCalendarUrl} />
|
|
190
202
|
</div>
|
|
191
203
|
);
|
|
@@ -116,7 +116,8 @@ export function EventTypeEditor({ eventTypeId, onSaved, onDeleted, className }:
|
|
|
116
116
|
const valid = form.title.trim().length > 0 && form.length >= 5 && (slugTouched ? form.slug.trim().length > 0 : true);
|
|
117
117
|
|
|
118
118
|
if (existing.error) return <ApiError error={existing.error} title="Could not load the event type" onRetry={() => void existing.refetch()} />;
|
|
119
|
-
|
|
119
|
+
// Also while the NEXT event type loads (the hook keeps the previous one's data meanwhile): the form is blank then, and saving it would blank the record.
|
|
120
|
+
if (eventTypeId != null && (existing.loading || !existing.data)) return <Skeleton className="h-64 w-full" aria-label="Loading event type" />;
|
|
120
121
|
|
|
121
122
|
return (
|
|
122
123
|
<form
|
|
@@ -21,6 +21,8 @@ import { g8Get } from "@/g8/lib/g8-client";
|
|
|
21
21
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
22
22
|
import { formatDateTime, listOf, unwrap, type Booking, type EventType } from "../lib/appointments-types";
|
|
23
23
|
import { MeetingDetail } from "./meeting-detail";
|
|
24
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
25
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
24
26
|
|
|
25
27
|
export function EventTypeOverview({ eventTypeId }: { eventTypeId: number }) {
|
|
26
28
|
const q = useG8Query(async (signal) => unwrap<EventType>(await g8Get(`/appointments/event-types/${eventTypeId}`, undefined, { signal })), [eventTypeId]);
|
|
@@ -52,8 +54,8 @@ export function EventTypeBookings({ eventTypeId, pageSize = 20 }: { eventTypeId:
|
|
|
52
54
|
<TableHeader><TableRow><TableHead>When</TableHead><TableHead>Attendees</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
|
|
53
55
|
<TableBody>
|
|
54
56
|
{q.data.map((b) => (
|
|
55
|
-
<TableRow key={b.uid} className="cursor-pointer"
|
|
56
|
-
<TableCell><
|
|
57
|
+
<TableRow key={b.uid} className="cursor-pointer" {...rowActivationProps(() => setOpen(b.uid))}>
|
|
58
|
+
<TableCell><RecordLink id={b.uid} onOpen={() => setOpen(b.uid)}>{formatDateTime(b.start_time)}</RecordLink></TableCell>
|
|
57
59
|
<TableCell className="max-w-64 truncate">{b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees"}</TableCell>
|
|
58
60
|
<TableCell><StatusPill tone={toneForStatus(b.status)} dot>{humanizeStatus(b.status)}</StatusPill></TableCell>
|
|
59
61
|
</TableRow>
|
|
@@ -19,11 +19,15 @@ import { EmptyState } from "@/g8/components/empty-state";
|
|
|
19
19
|
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
20
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
21
|
import { formatDateTime, listOf, type Booking, type RecordedMeeting } from "../lib/appointments-types";
|
|
22
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
23
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
22
24
|
|
|
23
25
|
export type MeetingView = "upcoming" | "past" | "cancelled" | "recorded";
|
|
24
26
|
export type MeetingSelection = { kind: "booking"; id: string } | { kind: "meeting"; id: string };
|
|
25
27
|
|
|
26
28
|
export interface MeetingListProps {
|
|
29
|
+
/** URL param the host screen reads for this record type. With it the name is a real link (Cmd-click opens it in a new tab); without it a plain button. */
|
|
30
|
+
linkParam?: string;
|
|
27
31
|
onSelect?: (selection: MeetingSelection) => void;
|
|
28
32
|
initialView?: MeetingView;
|
|
29
33
|
pageSize?: number;
|
|
@@ -58,7 +62,7 @@ function useDebouncedText(value: string, ms = 300): string {
|
|
|
58
62
|
return v;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
|
|
65
|
+
export function MeetingList({ linkParam, onSelect, initialView = "upcoming", pageSize = 20, refreshKey, selectedId, className }: MeetingListProps) {
|
|
62
66
|
const [view, setView] = useState<MeetingView>(initialView);
|
|
63
67
|
const [page, setPage] = useState(0);
|
|
64
68
|
const [search, setSearch] = useState("");
|
|
@@ -120,17 +124,17 @@ export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20,
|
|
|
120
124
|
<TableBody>
|
|
121
125
|
{result.kind === "booking"
|
|
122
126
|
? (result.items as Booking[]).map((b) => (
|
|
123
|
-
<TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer"
|
|
127
|
+
<TableRow key={b.uid} data-state={selectedId === b.uid ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "booking", id: b.uid })) : {})}>
|
|
124
128
|
<TableCell className="whitespace-nowrap">{formatDateTime(b.start_time)}</TableCell>
|
|
125
|
-
<TableCell className="font-medium">{b.title || b.event_type_title || "Meeting"}</TableCell>
|
|
129
|
+
<TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={`booking:${b.uid}`} onOpen={() => onSelect({ kind: "booking", id: b.uid })}>{b.title || b.event_type_title || "Meeting"}</RecordLink> : b.title || b.event_type_title || "Meeting"}</TableCell>
|
|
126
130
|
<TableCell className="max-w-64 truncate">{attendeeNames(b)}</TableCell>
|
|
127
131
|
<TableCell className="capitalize">{b.status}</TableCell>
|
|
128
132
|
</TableRow>
|
|
129
133
|
))
|
|
130
134
|
: (result.items as RecordedMeeting[]).map((m) => (
|
|
131
|
-
<TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer"
|
|
135
|
+
<TableRow key={m.id} data-state={selectedId === m.id ? "selected" : undefined} className="cursor-pointer" {...(onSelect ? rowActivationProps(() => onSelect({ kind: "meeting", id: m.id })) : {})}>
|
|
132
136
|
<TableCell className="whitespace-nowrap">{formatDateTime(m.start_time)}</TableCell>
|
|
133
|
-
<TableCell className="font-medium">{m.subject || "Untitled meeting"}</TableCell>
|
|
137
|
+
<TableCell className="font-medium">{onSelect ? <RecordLink param={linkParam} id={`meeting:${m.id}`} onOpen={() => onSelect({ kind: "meeting", id: m.id })}>{m.subject || "Untitled meeting"}</RecordLink> : m.subject || "Untitled meeting"}</TableCell>
|
|
134
138
|
<TableCell className="max-w-64 truncate">{m.transcript_summary || "No summary yet"}</TableCell>
|
|
135
139
|
<TableCell className="capitalize">{m.transcript_status ?? "none"}</TableCell>
|
|
136
140
|
</TableRow>
|
|
@@ -40,6 +40,7 @@ import { listOf, type Booking } from "../lib/appointments-types";
|
|
|
40
40
|
import { BookingPage } from "./booking-page";
|
|
41
41
|
import { MeetingDetail } from "./meeting-detail";
|
|
42
42
|
import { MeetingList, type MeetingSelection } from "./meeting-list";
|
|
43
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
43
44
|
|
|
44
45
|
export interface MeetingsScreenProps {
|
|
45
46
|
/** Where a person connects a calendar (graph8 app). */
|
|
@@ -99,7 +100,9 @@ function ScheduleSheet({ open, onOpenChange, onBooked }: { open: boolean; onOpen
|
|
|
99
100
|
);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
export function MeetingsScreen({ connectCalendarUrl
|
|
103
|
+
export function MeetingsScreen({ connectCalendarUrl: connectCalendarUrlProp, renderDetailActions, extraDetailTabs, toolbarActions }: MeetingsScreenProps) {
|
|
104
|
+
const brand = useBrand();
|
|
105
|
+
const connectCalendarUrl = connectCalendarUrlProp ?? brand.appUrl;
|
|
103
106
|
const [raw, setRaw] = useUrlState("sel");
|
|
104
107
|
const [mode, setMode] = useUrlState("view");
|
|
105
108
|
const selection = parseSelection(raw);
|
|
@@ -143,10 +146,10 @@ export function MeetingsScreen({ connectCalendarUrl = "https://app.graph8.com",
|
|
|
143
146
|
expanded={mode === "full" && Boolean(selection)}
|
|
144
147
|
onExpandedChange={(full) => setMode(full ? "full" : null)}
|
|
145
148
|
detail={detail}
|
|
146
|
-
list={<MeetingList refreshKey={version} selectedId={selection?.id ?? null} onSelect={(s) => setRaw(`${s.kind}:${s.id}`)} />}
|
|
149
|
+
list={<MeetingList linkParam="sel" refreshKey={version} selectedId={selection?.id ?? null} onSelect={(s) => setRaw(`${s.kind}:${s.id}`)} />}
|
|
147
150
|
/>
|
|
148
|
-
<SectionCard title="Calendar sync" description=
|
|
149
|
-
<a className="inline-flex items-center gap-1 text-sm underline" href={connectCalendarUrl} target="_blank" rel="noreferrer">Connect a calendar in
|
|
151
|
+
<SectionCard title="Calendar sync" description={`Google and Microsoft sign-in happens in the ${brand.linkLabel} app. Once connected, booked and recorded meetings appear here.`}>
|
|
152
|
+
<a className="inline-flex items-center gap-1 text-sm underline" href={connectCalendarUrl} target="_blank" rel="noreferrer">Connect a calendar in {brand.linkLabel}<ExternalLink className="size-3.5" aria-hidden /></a>
|
|
150
153
|
</SectionCard>
|
|
151
154
|
<ScheduleSheet open={scheduling} onOpenChange={setScheduling} onBooked={changed} />
|
|
152
155
|
</ScreenLayout>
|
|
@@ -37,6 +37,7 @@ import { AvailabilityEditor } from "./availability-editor";
|
|
|
37
37
|
import { BookingPage } from "./booking-page";
|
|
38
38
|
import { EventTypeEditor } from "./event-type-editor";
|
|
39
39
|
import { EventTypeBookings, EventTypeOverview } from "./event-type-related";
|
|
40
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
40
41
|
|
|
41
42
|
export interface SchedulingScreenProps {
|
|
42
43
|
/** Where a person connects a calendar (graph8 app). */
|
|
@@ -88,7 +89,7 @@ export function SchedulingScreen({ connectCalendarUrl, renderRowActions, renderD
|
|
|
88
89
|
<TableHeader><TableRow><TableHead>Event type</TableHead><TableHead className="text-right">Length</TableHead><TableHead>Rules</TableHead>{renderRowActions ? <TableHead /> : null}</TableRow></TableHeader>
|
|
89
90
|
<TableBody>
|
|
90
91
|
{types.eventTypes.map((t) => (
|
|
91
|
-
<TableRow key={t.id} className="cursor-pointer" data-state={String(t.id) === idParam ? "selected" : undefined}
|
|
92
|
+
<TableRow key={t.id} className="cursor-pointer" data-state={String(t.id) === idParam ? "selected" : undefined} {...rowActivationProps(() => setId(String(t.id)))}>
|
|
92
93
|
<TableCell><button type="button" className="font-medium hover:underline" onClick={(e) => { e.stopPropagation(); setId(String(t.id)); }}>{t.title}</button><div className="text-xs text-muted-foreground">/{t.slug}</div></TableCell>
|
|
93
94
|
<TableCell className="text-right tabular-nums">{t.length} min</TableCell>
|
|
94
95
|
<TableCell className="space-x-1">
|
package/blocks/block.schema.json
CHANGED
|
@@ -36,6 +36,24 @@
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
+
"screens": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "Optional. How the block's finished screens become routes and sidebar entries in an app made with `create --blocks`. Absent: derived from the components named `*Screen` (ContactsScreen -> /contacts, label Contacts, group = the block title, icon from a built-in per-block map).",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"required": ["component", "path", "label"],
|
|
46
|
+
"properties": {
|
|
47
|
+
"component": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$", "description": "A component of this block." },
|
|
48
|
+
"path": { "type": "string", "pattern": "^/[a-z0-9]+(-[a-z0-9]+)*(/[a-z0-9]+(-[a-z0-9]+)*)*$", "description": "Static route path. On a collision in a generated app it is prefixed with the block id." },
|
|
49
|
+
"label": { "type": "string", "minLength": 1 },
|
|
50
|
+
"icon": { "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$", "description": "A lucide-react export name." },
|
|
51
|
+
"group": { "type": "string", "minLength": 1, "description": "Sidebar section (default: the block title)." },
|
|
52
|
+
"description": { "type": "string", "minLength": 1 },
|
|
53
|
+
"order": { "type": "number", "description": "Sort position inside the block; lower first." }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
39
57
|
"hooks": {
|
|
40
58
|
"type": "array",
|
|
41
59
|
"items": {
|
package/blocks/block.types.ts
CHANGED
|
@@ -59,6 +59,21 @@ export interface BlockDefault {
|
|
|
59
59
|
askHuman: boolean;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Optional route metadata for a finished screen (`create --blocks`, `add` inside a created app). */
|
|
63
|
+
export interface BlockScreen {
|
|
64
|
+
/** A component of this block. */
|
|
65
|
+
component: string;
|
|
66
|
+
/** Static route path; prefixed with the block id when it collides in a generated app. */
|
|
67
|
+
path: string;
|
|
68
|
+
label: string;
|
|
69
|
+
/** lucide-react export name. */
|
|
70
|
+
icon?: string;
|
|
71
|
+
/** Sidebar section (default: the block title). */
|
|
72
|
+
group?: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
order?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
export interface BlockManifest {
|
|
63
78
|
$schema?: string;
|
|
64
79
|
id: string;
|
|
@@ -71,6 +86,8 @@ export interface BlockManifest {
|
|
|
71
86
|
/** Contract operationIds (keys of API_OPERATIONS). */
|
|
72
87
|
operations: string[];
|
|
73
88
|
components: BlockComponent[];
|
|
89
|
+
/** Optional. Absent: derived from the components named `*Screen`. */
|
|
90
|
+
screens?: BlockScreen[];
|
|
74
91
|
hooks: BlockHook[];
|
|
75
92
|
lib?: BlockFile[];
|
|
76
93
|
server?: BlockServerFile[];
|
|
@@ -7,6 +7,26 @@ Apollo / HubSpot "campaigns" screen, with generation behind credit confirmations
|
|
|
7
7
|
Depends on: `lists` (the audience picker), `mailboxes` (senders and the connect-a-mailbox link). Install:
|
|
8
8
|
`npx @graph8/sdk add campaigns`.
|
|
9
9
|
|
|
10
|
+
## Screens
|
|
11
|
+
|
|
12
|
+
`CampaignsScreen` is the finished page: mount it as the route. Every record type has a complete detail view,
|
|
13
|
+
deep-linkable (`?tab=campaigns|profile|personas`, `?id=` the record, `?dtab=` the tab, `?doc=` a document).
|
|
14
|
+
|
|
15
|
+
- **Campaigns tab**: list (delete confirmed) beside a detail panel with Details (every field, audience, edit name,
|
|
16
|
+
goal and brief, confirmed), Metrics, Documents (each document opens with its fields and editor), Audience,
|
|
17
|
+
Sequence and Launch. "New campaign" runs the five-step wizard in a sheet.
|
|
18
|
+
- **Company profile tab**: the profile plus ICPs; `IcpDetail` shows every field and edits, pins or archives.
|
|
19
|
+
- **Personas tab**: `PersonaDetail` shows every field, the campaigns that target it, and edits, pins or archives.
|
|
20
|
+
|
|
21
|
+
Slot props: `toolbarActions`, `renderDetailActions(campaign)`, `extraDetailTabs`, `websiteUrl`. Prerequisites: a
|
|
22
|
+
company profile with the real website, an active mailbox, a contact list, and credits for AI generation.
|
|
23
|
+
`IcpEditor` and `PersonaEditor` gained optional `onOpen` and `refreshKey`; `CampaignAssets` optional
|
|
24
|
+
`documentId` and `onDocumentChange`.
|
|
25
|
+
|
|
26
|
+
Known limits: there is no single-ICP read, so `IcpDetail` finds the ICP in the first 200 (the API maximum).
|
|
27
|
+
`PersonaDetail` lists the campaigns whose `target_persona` matches the persona title among the first 100
|
|
28
|
+
campaigns; the API does not link campaigns to ICPs, so ICPs show none.
|
|
29
|
+
|
|
10
30
|
## Components
|
|
11
31
|
|
|
12
32
|
| Component | What it does | Tiers |
|
|
@@ -16,7 +36,7 @@ Depends on: `lists` (the audience picker), `mailboxes` (senders and the connect-
|
|
|
16
36
|
| `CampaignWizard` | A page. Idea (AI, polled) -> create -> audience -> steps -> launch. Also exports the three step components below. | all |
|
|
17
37
|
| `CampaignAudienceStep` | Attach or detach a contact list (uses the lists block `ListPicker`; reads `getCampaignFull`, since `getCampaign` omits the audience). | read, write, external |
|
|
18
38
|
| `CampaignSequenceStep` | Add, edit, remove and AI-write sequence steps. | read, billable, external, destructive |
|
|
19
|
-
| `CampaignLaunchStep` | Readiness checklist,
|
|
39
|
+
| `CampaignLaunchStep` | Readiness checklist (audience, steps, written copy, sender), deliverability and warm-up hold warnings, writes missing email copy with AI, confirmed launch. | read, billable, external |
|
|
20
40
|
| `CampaignList` | A page. All campaigns; open one for metrics and documents; delete. | read, billable, external, destructive |
|
|
21
41
|
| `CampaignAssets` | Campaign documents: read, edit, rewrite with AI, add, delete. Standalone: shows a campaign picker. | read, billable, external, destructive |
|
|
22
42
|
| `CampaignMetrics` | A page. Metrics for 7, 30, 90 or 365 days. Standalone: shows a campaign picker. | read |
|
|
@@ -40,15 +60,24 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
|
|
|
40
60
|
3. **Ideas.** `generateCampaignIdeasAsync` (billable) -> `task_id` -> poll `getCampaignIdeasTaskStatus` every
|
|
41
61
|
2-3 seconds until `completed`; `ideas[]` carry `campaign_name`, `core_concept`, `primary_hook`.
|
|
42
62
|
4. **Create.** `createCampaign` -> `id`. Store it at once; later steps reuse it and "Create" becomes
|
|
43
|
-
"Save changes" (`updateCampaign`). With `auto_generate_documents`
|
|
44
|
-
background: poll `getCampaignGenerationStatus
|
|
63
|
+
"Save changes" (`updateCampaign`). With `auto_generate_documents` (on by default in the wizard and the
|
|
64
|
+
recipe) the documents generate in the background: poll `getCampaignGenerationStatus`; a `generation_error`
|
|
65
|
+
there, or `generation_status: "failed_to_dispatch"` on create, means the writing never started.
|
|
45
66
|
5. **Audience.** `getCampaignFull` shows what is attached (`audience_list_id`, `audience`). `ListPicker` (lists block) -> a list `id` -> `attachAudienceToCampaign` with `list_id` as a
|
|
46
67
|
string. Build the list in the search/lists blocks first.
|
|
47
68
|
6. **Steps and copy.** `getCampaignSequence` -> `createCampaignStep` (channel -> mode matrix: email -> email,
|
|
48
69
|
phone -> call|voicemail, sms -> sms, social -> connect_note|dm|comment|reply) -> `generateCampaignStepContent`
|
|
49
|
-
(billable) per step. `CampaignAssets` edits the documents behind them.
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
(billable) per step. `CampaignAssets` edits the documents behind them. Writing an EMAIL step needs the
|
|
71
|
+
campaign's `emails` document to exist already (created by `auto_generate_documents`); without it the step
|
|
72
|
+
write answers 404.
|
|
73
|
+
7. **Launch.** `CampaignLaunchStep` -> `getCampaignFull` (audience attached with its size, >= 1 step, written copy,
|
|
74
|
+
`is_launched`), `listMailboxes` (active ones), `getSafeToSend` (`safe`, a warning) -> `launchCampaign` with
|
|
75
|
+
`sender_mailbox_ids`. The launch is refused (400 "no completed copy documents") unless a document other than
|
|
76
|
+
`sequence` and `step_catalog` has `status: "completed"`, so Launch stays disabled until one does; "Write the
|
|
77
|
+
email copy with AI" creates the `emails` document if missing (`createCampaignDocument`, once) and has AI write it
|
|
78
|
+
(`regenerateCampaignDocument`, billable). `is_launched` is true only for a live launch, so a failed launch can be
|
|
79
|
+
retried. A mailbox added less than 14 days ago is in its warm-up hold: the campaign goes live but sends nothing
|
|
80
|
+
from it until the hold ends (the step warns).
|
|
52
81
|
The one-step launch executes email only; configure other channels in the sequencer block.
|
|
53
82
|
8. **Measure.** `CampaignMetrics` -> `getCampaignMetrics` with `days`.
|
|
54
83
|
|
|
@@ -62,8 +91,9 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
|
|
|
62
91
|
|
|
63
92
|
## Defaults (do not ask the human)
|
|
64
93
|
|
|
65
|
-
- 5 ideas, goal `awareness`, channel `email`,
|
|
66
|
-
|
|
94
|
+
- 5 ideas, goal `awareness`, channel `email`, AI-written documents ON (a campaign without written copy cannot
|
|
95
|
+
launch; the create confirmation shows the spend), personas picked by AI when none are chosen
|
|
96
|
+
(`persona_mode: "ai_pick"`), sender = the first active mailbox.
|
|
67
97
|
- **Ask the human**: whether to launch and to which list (state the recipient count), and whether to approve
|
|
68
98
|
(lock) the company profile.
|
|
69
99
|
|
|
@@ -95,7 +125,7 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
|
|
|
95
125
|
| `get_campaign_ideas_task_status_campaigns_ideas_status__task_id__get` | GET /campaigns/ideas/status/{task_id} | read | CampaignWizard |
|
|
96
126
|
| `create_campaign_campaigns_post` | POST /campaigns | external | CampaignWizard |
|
|
97
127
|
| `update_campaign_campaigns__campaign_id__patch` | PATCH /campaigns/{campaign_id} | external | CampaignWizard |
|
|
98
|
-
| `get_campaign_generation_status_campaigns__campaign_id__status_get` | GET /campaigns/{campaign_id}/status | read | CampaignWizard |
|
|
128
|
+
| `get_campaign_generation_status_campaigns__campaign_id__status_get` | GET /campaigns/{campaign_id}/status | read | CampaignWizard, CampaignLaunchStep |
|
|
99
129
|
| `get_campaign_full_campaigns__campaign_id__full_get` | GET /campaigns/{campaign_id}/full | read | CampaignWizard, CampaignAudienceStep, CampaignLaunchStep |
|
|
100
130
|
| `attach_audience_to_campaign_campaigns__campaign_id__audience_put` | PUT /campaigns/{campaign_id}/audience | external | CampaignWizard, CampaignAudienceStep |
|
|
101
131
|
| `get_campaign_sequence_campaigns__campaign_id__sequence_get` | GET /campaigns/{campaign_id}/sequence | read | CampaignWizard, CampaignSequenceStep |
|
|
@@ -112,7 +142,7 @@ read, so credit numbers are estimates: edit `CREDIT_ESTIMATES` in `lib/campaigns
|
|
|
112
142
|
| `get_campaign_metrics_campaigns__campaign_id__metrics_get` | GET /campaigns/{campaign_id}/metrics | read | CampaignList, CampaignMetrics |
|
|
113
143
|
| `list_campaign_documents_campaigns__campaign_id__documents_get` | GET /campaigns/{campaign_id}/documents | read | CampaignList, CampaignAssets |
|
|
114
144
|
| `get_campaign_document_campaigns__campaign_id__documents__document_id__get` | GET /campaigns/{campaign_id}/documents/{document_id} | read | CampaignList, CampaignAssets |
|
|
115
|
-
| `create_campaign_document_campaigns__campaign_id__documents_post` | POST /campaigns/{campaign_id}/documents | external | CampaignList, CampaignAssets |
|
|
145
|
+
| `create_campaign_document_campaigns__campaign_id__documents_post` | POST /campaigns/{campaign_id}/documents | external | CampaignWizard, CampaignLaunchStep, CampaignList, CampaignAssets |
|
|
116
146
|
| `update_campaign_document_campaigns__campaign_id__documents__document_id__put` | PUT /campaigns/{campaign_id}/documents/{document_id} | external | CampaignList, CampaignAssets |
|
|
117
147
|
| `delete_campaign_document_campaigns__campaign_id__documents__document_id__delete` | DELETE /campaigns/{campaign_id}/documents/{document_id} | destructive | CampaignList, CampaignAssets |
|
|
118
|
-
| `regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post` | POST /campaigns/{campaign_id}/documents/{document_id}/regenerate | billable | CampaignList, CampaignAssets |
|
|
148
|
+
| `regenerate_campaign_document_campaigns__campaign_id__documents__document_id__regenerate_post` | POST /campaigns/{campaign_id}/documents/{document_id}/regenerate | billable | CampaignWizard, CampaignLaunchStep, CampaignList, CampaignAssets |
|