@graph8/sdk 0.373.0 → 0.374.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/blocks/_foundation/README.md +47 -0
- package/blocks/_foundation/block.json +13 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/company-intelligence/components/research-run.tsx +6 -2
- package/blocks/crm/README.md +19 -0
- package/blocks/crm/block.json +205 -6
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +47 -6
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +46 -5
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +1 -0
- package/blocks/deals/block.json +17 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-detail.tsx +5 -3
- package/blocks/dialer/components/call-log-screen.tsx +6 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +22 -4
- package/blocks/enrichment/block.json +145 -5
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/lib/enrichment-types.ts +12 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/forms/README.md +2 -0
- package/blocks/forms/components/forms-screen.tsx +6 -3
- package/blocks/inbox/README.md +40 -4
- package/blocks/inbox/block.json +130 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-view.tsx +2 -0
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
- package/blocks/pages/block.json +10 -1
- package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
- package/blocks/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +25 -1
- package/blocks/quotes/block.json +123 -6
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-list.tsx +23 -5
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +16 -3
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/sequencer/README.md +55 -0
- package/blocks/sequencer/block.json +172 -5
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/sequence-builder.tsx +13 -3
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +6 -0
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +36 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +10 -1
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/visitors/components/traffic-chart.tsx +3 -2
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +70 -7
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +50 -17
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/dist/cli.js +257 -51
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/create.d.ts +5 -2
- package/dist/types/version.d.ts +2 -2
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
- package/stacks/6sense.json +20 -0
- package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
- package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
- package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
- package/stacks/apollo.json +33 -2
- package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
- package/stacks/clay/pages/lists.tsx.tmpl +14 -2
- package/stacks/gong/pages/calls.tsx.tmpl +9 -9
- package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
- package/stacks/hootsuite.json +10 -0
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
- package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +45 -2
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
- package/stacks/intercom.json +10 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp.json +2 -1
- package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
- package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
- package/stacks/outreach.json +13 -2
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
- package/stacks/pipedrive.json +10 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
- package/stacks/salesforce.json +10 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
- package/stacks/zoominfo.json +20 -0
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PipelineTemplates: pick a ready-made pipeline for a list (for example "Find + verify missing emails") and add it.
|
|
5
|
+
* The catalog is filtered by the API to the recipes that fit this list's kind (people or companies).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listPipelineTemplates GET /enrichment/lists/pipeline-templates?list_id (read, free) recipes + whether the workspace has a
|
|
9
|
+
* qualification template (recipes that score records first need one)
|
|
10
|
+
* 2. createListPipelineFromTemplate POST /enrichment/lists/{list_id}/pipelines/from-template { template_key } (write, free)
|
|
11
|
+
* saves the recipe TURNED OFF and creates any columns it needs. Adding a recipe the list already has REPLACES that
|
|
12
|
+
* pipeline's steps with the recipe's (it is saved by name), so that case goes through ConfirmSpendDialog (destructive).
|
|
13
|
+
*
|
|
14
|
+
* Nothing runs or charges here: open the new pipeline to price and run it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
import { Button } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
22
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
23
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { itemsOf, templateExists } from "../lib/pipeline-types";
|
|
28
|
+
import type { ListPipeline, PipelineTemplate } from "../lib/pipeline-types";
|
|
29
|
+
|
|
30
|
+
export interface PipelineTemplatesProps {
|
|
31
|
+
listId: number;
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange: (open: boolean) => void;
|
|
34
|
+
/** The pipelines already on the list, to warn before a recipe replaces one. */
|
|
35
|
+
existing: ListPipeline[];
|
|
36
|
+
/** Called with the saved pipeline. */
|
|
37
|
+
onAdded: (pipeline: ListPipeline) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Catalog { items?: PipelineTemplate[]; has_qualification_templates?: boolean }
|
|
41
|
+
|
|
42
|
+
export function PipelineTemplates({ listId, open, onOpenChange, existing, onAdded }: PipelineTemplatesProps) {
|
|
43
|
+
const [replacing, setReplacing] = useState<PipelineTemplate | null>(null);
|
|
44
|
+
const [working, setWorking] = useState<string | null>(null);
|
|
45
|
+
const catalog = useG8Query(async (signal) => (await g8Get<Catalog>("/enrichment/lists/pipeline-templates", { list_id: listId }, { signal })).data, [listId], { enabled: open });
|
|
46
|
+
const add = useG8Mutation(
|
|
47
|
+
async (t: PipelineTemplate) => (await g8Post<ListPipeline>(`/enrichment/lists/${listId}/pipelines/from-template`, { template_key: t.key })).data,
|
|
48
|
+
{ onSuccess: (p) => { setReplacing(null); setWorking(null); onAdded(p); onOpenChange(false); }, onError: () => { setWorking(null); setReplacing(null); } },
|
|
49
|
+
);
|
|
50
|
+
const templates = itemsOf<PipelineTemplate>(catalog.data);
|
|
51
|
+
const hasQualification = catalog.data?.has_qualification_templates === true;
|
|
52
|
+
const choose = (t: PipelineTemplate) => {
|
|
53
|
+
if (templateExists(t, existing)) { setReplacing(t); return; }
|
|
54
|
+
setWorking(t.key);
|
|
55
|
+
void add.mutate(t);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<FormSheet open={open} onOpenChange={onOpenChange} title="Start from a template" description="Ready-made pipelines for this list. They are saved turned off; nothing runs until you run one." width="lg"
|
|
60
|
+
footer={<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>Close</Button>}>
|
|
61
|
+
{catalog.error ? <ApiError error={catalog.error} title="Could not load templates" onRetry={() => void catalog.refetch()} /> : null}
|
|
62
|
+
{catalog.loading && !catalog.data ? <p className="text-sm text-muted-foreground">Loading templates...</p> : null}
|
|
63
|
+
{!catalog.loading && !catalog.error && templates.length === 0 ? <EmptyState title="No templates for this list" description="The API has no ready-made pipeline for this kind of list." /> : null}
|
|
64
|
+
{add.error ? <ApiError error={add.error} title="Could not add the template" /> : null}
|
|
65
|
+
<ul className="flex flex-col gap-3">
|
|
66
|
+
{templates.map((t) => {
|
|
67
|
+
const needsQualification = t.requires_qualification && !hasQualification;
|
|
68
|
+
return (
|
|
69
|
+
<li key={t.key} className="rounded-md border p-3" data-testid="template">
|
|
70
|
+
<div className="flex items-start justify-between gap-3">
|
|
71
|
+
<div className="min-w-0">
|
|
72
|
+
<p className="text-sm font-medium">{t.name}</p>
|
|
73
|
+
<p className="text-xs text-muted-foreground">{t.description}</p>
|
|
74
|
+
<ol className="mt-2 flex flex-wrap gap-1" aria-label={`${t.name} steps`}>{t.step_names.map((n, i) => <li key={`${n}-${i}`}><StatusPill tone="neutral">{i + 1}. {n}</StatusPill></li>)}</ol>
|
|
75
|
+
{needsQualification ? <p className="mt-2 text-xs text-warning-foreground">Needs a qualification template, and this workspace has none yet.</p> : null}
|
|
76
|
+
{templateExists(t, existing) ? <p className="mt-2 text-xs text-muted-foreground">This list already has it. Adding it again resets its steps to the recipe.</p> : null}
|
|
77
|
+
</div>
|
|
78
|
+
<Button type="button" size="sm" variant="outline" disabled={needsQualification || working != null} loading={working === t.key} aria-label={`Add ${t.name}`} onClick={() => choose(t)}>{templateExists(t, existing) ? "Reset" : "Add"}</Button>
|
|
79
|
+
</div>
|
|
80
|
+
</li>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</ul>
|
|
84
|
+
<ConfirmSpendDialog open={replacing != null} tier="destructive" title={`Reset "${replacing?.name ?? ""}" to the template`}
|
|
85
|
+
description="The pipeline's steps are replaced with the template's. Any edits you made to its steps are lost. Values it already wrote stay on the records."
|
|
86
|
+
affected={1} confirmLabel="Reset pipeline" confirming={add.loading}
|
|
87
|
+
onConfirm={() => replacing && void add.mutate(replacing)} onCancel={() => setReplacing(null)} />
|
|
88
|
+
</FormSheet>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -104,6 +104,7 @@ export interface EnrichJob {
|
|
|
104
104
|
successful_enrichments?: number | null;
|
|
105
105
|
failed_enrichments?: number | null;
|
|
106
106
|
total_credits_used?: number | null;
|
|
107
|
+
paused_at?: string | null;
|
|
107
108
|
warnings?: string[] | null;
|
|
108
109
|
provider_errors?: Array<{ record_id?: number; provider?: string; error?: string }> | null;
|
|
109
110
|
}
|
|
@@ -129,6 +130,17 @@ export function isJobTerminal(status: string | undefined | null): boolean {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
/**
|
|
133
|
+
* The API's own quote for a run (`max_total_credits` from the preflight), or null when it could not price it. Null means
|
|
134
|
+
* "no total": callers show that, never a made-up number.
|
|
135
|
+
*/
|
|
136
|
+
export function quotedCredits(preflight: Pick<Preflight, "max_total_credits"> | undefined): number | null {
|
|
137
|
+
return preflight?.max_total_credits ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @deprecated Guesses 2 credits per record when the preflight cannot price a run, which is an invented price. The bulk panel
|
|
142
|
+
* no longer uses it (see `quotedCredits`); kept exported so existing imports keep working.
|
|
143
|
+
*
|
|
132
144
|
* What to show as the cost of a run: the API's own quote when the preflight priced it, otherwise the flat
|
|
133
145
|
* per-record estimate times the contacts that are really on the list. Never a balance.
|
|
134
146
|
*/
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shapes and pure helpers for list pipelines, auto-enrichment and the enrichment activity view.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is read off the API's own responses (enrichment/interfaces/fastapi/list_pipelines_router.py,
|
|
7
|
+
* auto_enrichment_router.py, router.py). The contract types these payloads as open objects, so the shapes below are the
|
|
8
|
+
* handlers' documented fields; a field the API leaves out is `undefined`/`null` here and the UI shows a dash, never a
|
|
9
|
+
* guessed default.
|
|
10
|
+
*
|
|
11
|
+
* No prices live here: the cost of a pipeline run is whatever `POST .../estimate` answers for that exact list and
|
|
12
|
+
* pipeline (`required_credits`, an upper bound that ignores run conditions).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The step kinds a saved pipeline step can have. */
|
|
16
|
+
export type StepType = "ai" | "waterfall";
|
|
17
|
+
|
|
18
|
+
export interface PipelineStep {
|
|
19
|
+
id: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
type: StepType;
|
|
22
|
+
config_ref: string;
|
|
23
|
+
run_condition?: string | null;
|
|
24
|
+
skip_existing_values?: boolean;
|
|
25
|
+
skip_recently_enriched?: boolean;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
ui_meta?: Record<string, unknown> | null;
|
|
28
|
+
[extra: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StepProgress {
|
|
32
|
+
step_index: number;
|
|
33
|
+
step_id?: string | null;
|
|
34
|
+
name?: string | null;
|
|
35
|
+
job_id?: string | null;
|
|
36
|
+
status?: string | null;
|
|
37
|
+
total_records?: number;
|
|
38
|
+
processed_records?: number;
|
|
39
|
+
successful?: number;
|
|
40
|
+
failed?: number;
|
|
41
|
+
skipped_by_reason?: Record<string, number>;
|
|
42
|
+
note?: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PipelineRun {
|
|
46
|
+
run_id: string;
|
|
47
|
+
status: string;
|
|
48
|
+
current_index: number;
|
|
49
|
+
total_steps: number;
|
|
50
|
+
started_at?: string | null;
|
|
51
|
+
error?: string | null;
|
|
52
|
+
cancelled_by?: string | null;
|
|
53
|
+
step_progress?: StepProgress[];
|
|
54
|
+
/** Present on a run read by id (GET .../pipeline-runs/{run_id}). */
|
|
55
|
+
pipeline_id?: string;
|
|
56
|
+
pipeline_name?: string;
|
|
57
|
+
list_id?: number;
|
|
58
|
+
current_job_id?: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ListPipeline {
|
|
62
|
+
id: string;
|
|
63
|
+
list_id: number;
|
|
64
|
+
name: string;
|
|
65
|
+
steps: PipelineStep[];
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
created_at?: string | null;
|
|
68
|
+
last_run?: PipelineRun | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PipelineEstimate {
|
|
72
|
+
can_run: boolean;
|
|
73
|
+
subscription_valid?: boolean;
|
|
74
|
+
estimated_evaluations: number;
|
|
75
|
+
required_credits: number;
|
|
76
|
+
available_credits: number;
|
|
77
|
+
reason?: string | null;
|
|
78
|
+
steps: Array<{ step_id: string; name: string; type: string; estimated_records: number; required_credits: number }>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface StepOption {
|
|
82
|
+
type: StepType;
|
|
83
|
+
config_ref: string;
|
|
84
|
+
name: string;
|
|
85
|
+
is_qualification?: boolean;
|
|
86
|
+
qualification_entity?: string | null;
|
|
87
|
+
uses_existing_field?: boolean;
|
|
88
|
+
target_column_name?: string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PipelineTemplate {
|
|
92
|
+
key: string;
|
|
93
|
+
name: string;
|
|
94
|
+
description: string;
|
|
95
|
+
target_type: string;
|
|
96
|
+
requires_qualification: boolean;
|
|
97
|
+
step_names: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AutoConfig {
|
|
101
|
+
id: string;
|
|
102
|
+
audience_id: number;
|
|
103
|
+
enrichment_type: StepType;
|
|
104
|
+
column_id: string;
|
|
105
|
+
trigger_on_insert: boolean;
|
|
106
|
+
created_at?: string | null;
|
|
107
|
+
created_by?: string | null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const MERGE_RULES = ["sticky", "user_wins", "mashup_wins"] as const;
|
|
111
|
+
export type MergeRule = (typeof MERGE_RULES)[number];
|
|
112
|
+
|
|
113
|
+
/** What each merge rule does, from enrichment/usecases/apply_mashup_merge/handler.py. */
|
|
114
|
+
export const MERGE_RULE_COPY: Record<MergeRule, { label: string; help: string }> = {
|
|
115
|
+
sticky: { label: "Keep edits, fill the rest", help: "Fields a person edited are left alone; every other field takes the matched value." },
|
|
116
|
+
user_wins: { label: "Never overwrite", help: "No matched value is copied onto the record." },
|
|
117
|
+
mashup_wins: { label: "Matched data wins", help: "Every field takes the matched value, including fields a person edited." },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export interface AutoSettings {
|
|
121
|
+
entity_type: "contact" | "company";
|
|
122
|
+
enabled: boolean;
|
|
123
|
+
default_rule: MergeRule;
|
|
124
|
+
per_field_overrides: Record<string, MergeRule>;
|
|
125
|
+
sync_policy_version?: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface JobControlResult {
|
|
129
|
+
job_id?: string;
|
|
130
|
+
success?: boolean;
|
|
131
|
+
message?: string;
|
|
132
|
+
previous_status?: string;
|
|
133
|
+
paused_at?: string;
|
|
134
|
+
resumed_at?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The analytics windows the API accepts (anything else is a 422). */
|
|
138
|
+
export const PERIODS = [
|
|
139
|
+
{ value: "last_7_days", label: "Last 7 days" },
|
|
140
|
+
{ value: "last_30_days", label: "Last 30 days" },
|
|
141
|
+
{ value: "last_90_days", label: "Last 90 days" },
|
|
142
|
+
{ value: "current_month", label: "This month" },
|
|
143
|
+
{ value: "last_month", label: "Last month" },
|
|
144
|
+
] as const;
|
|
145
|
+
export type Period = (typeof PERIODS)[number]["value"];
|
|
146
|
+
|
|
147
|
+
export const GRANULARITIES = ["day", "week", "month"] as const;
|
|
148
|
+
export type Granularity = (typeof GRANULARITIES)[number];
|
|
149
|
+
|
|
150
|
+
/** Rows of an open `{ items }` payload, or of a bare array; anything else is an empty list. */
|
|
151
|
+
export function itemsOf<T>(data: unknown, key = "items"): T[] {
|
|
152
|
+
if (Array.isArray(data)) return data as T[];
|
|
153
|
+
const v = (data as Record<string, unknown> | null | undefined)?.[key];
|
|
154
|
+
return Array.isArray(v) ? (v as T[]) : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The only states a run never leaves (the app's useListPipelines treats everything else as in progress). */
|
|
158
|
+
export const RUN_TERMINAL = ["completed", "failed", "cancelled"] as const;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* A run that can still change: poll it, offer Stop, block another Run. The backend creates every run as "pending", so
|
|
162
|
+
* "active" is "has a status and it is not terminal", never "equals running": a second run priced and confirmed while the
|
|
163
|
+
* first is pending would charge twice.
|
|
164
|
+
*/
|
|
165
|
+
export function runIsActive(status: string | null | undefined): boolean {
|
|
166
|
+
const s = String(status ?? "").toLowerCase();
|
|
167
|
+
return s !== "" && !(RUN_TERMINAL as readonly string[]).includes(s);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The pipeline's steps that will run (a disabled step is skipped). */
|
|
171
|
+
export function enabledSteps(p: Pick<ListPipeline, "steps">): PipelineStep[] {
|
|
172
|
+
return p.steps.filter((s) => s.enabled !== false);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A unique, stable step id (1 to 64 characters) for a step added in the editor. */
|
|
176
|
+
export function newStepId(taken: readonly string[]): string {
|
|
177
|
+
let n = taken.length + 1;
|
|
178
|
+
while (taken.includes(`step-${n}`)) n += 1;
|
|
179
|
+
return `step-${n}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A step for a picked config. Defaults mirror the API's own: fill only empty values, skip recently enriched records. */
|
|
183
|
+
export function stepFromOption(option: StepOption, taken: readonly string[]): PipelineStep {
|
|
184
|
+
return { id: newStepId(taken), name: option.name, type: option.type, config_ref: option.config_ref, skip_existing_values: true, skip_recently_enriched: true, enabled: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Move the item at `index` by `delta` (clamped), returning a new array. */
|
|
188
|
+
export function moveItem<T>(items: readonly T[], index: number, delta: number): T[] {
|
|
189
|
+
const to = index + delta;
|
|
190
|
+
if (to < 0 || to >= items.length) return [...items];
|
|
191
|
+
const next = [...items];
|
|
192
|
+
[next[index], next[to]] = [next[to], next[index]];
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The body of a save: the name and the FULL step list (the API replaces steps), keeping every field of an edited step. */
|
|
197
|
+
export function savePayload(name: string, steps: readonly PipelineStep[]): { name: string; steps: PipelineStep[] } {
|
|
198
|
+
return { name: name.trim(), steps: steps.map((s) => ({ ...s })) };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Did the editor change anything the API would store? */
|
|
202
|
+
export function pipelineChanged(original: Pick<ListPipeline, "name" | "steps">, name: string, steps: readonly PipelineStep[]): boolean {
|
|
203
|
+
return original.name !== name.trim() || JSON.stringify(original.steps) !== JSON.stringify(steps);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The label of a step: its own name, else the name of the config it points at, else its kind. Never the raw id. */
|
|
207
|
+
export function stepLabel(step: PipelineStep, options: readonly StepOption[]): string {
|
|
208
|
+
const hit = options.find((o) => o.config_ref === step.config_ref && o.type === step.type);
|
|
209
|
+
return step.name?.trim() || hit?.name || (step.type === "ai" ? "AI step" : "Provider waterfall step");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** `n / total` as a whole percent, or null when there is nothing to divide by (shown as a dash, never 0%). */
|
|
213
|
+
export function percentOf(part: number | null | undefined, total: number | null | undefined): number | null {
|
|
214
|
+
if (part == null || !total) return null;
|
|
215
|
+
return Math.min(100, Math.round((part / total) * 100));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The reason a run reads as "skipped" in plain words. */
|
|
219
|
+
export function skipReasonLabel(reason: string): string {
|
|
220
|
+
if (reason === "run_condition_not_met") return "skipped by the step condition";
|
|
221
|
+
return reason.replace(/_/g, " ");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** True when the pipeline named like this template already exists on the list (re-adding it replaces that pipeline's steps). */
|
|
225
|
+
export function templateExists(template: Pick<PipelineTemplate, "name">, pipelines: readonly Pick<ListPipeline, "name">[]): boolean {
|
|
226
|
+
return pipelines.some((p) => p.name === template.name);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The most useful single sentence about why a pipeline cannot be run right now, or null when it can. */
|
|
230
|
+
export function runBlockedReason(p: Pick<ListPipeline, "steps" | "last_run">): string | null {
|
|
231
|
+
if (enabledSteps(p).length === 0) return "Every step is turned off, so there is nothing to run.";
|
|
232
|
+
if (runIsActive(p.last_run?.status)) return "A run is already in progress.";
|
|
233
|
+
return null;
|
|
234
|
+
}
|
package/blocks/forms/README.md
CHANGED
|
@@ -130,3 +130,5 @@ graph8 captures forms from your site; there is **no create-form operation in the
|
|
|
130
130
|
New on `FormsScreen`: a **Groups** tab (`FormGroups`: create, rename, re-type, add and remove forms, delete), saved views (`FormSavedViews`), "Load more" past the first 100 forms, and on an open form: a **Routing** tab (`FormBackfill`: preview, then a confirmed delivery; `FormRouteHistory`: restore the previous route or stop routing, both destructive confirms that send the version they read), an **Activity** tab (`FormActivity`: audit trail and pages), and **Export CSV** on Submissions (`SubmissionsExport`, formula-safe, says when the file is not the whole set).
|
|
131
131
|
|
|
132
132
|
Not covered (and why): `saveFormRoute` (the mapping editor's save already writes the route); `captureFormPreview` and `getFormPreview` return untyped image data; unidentified-form grouping (`previewUnidentifiedGrouping`, `setUnidentifiedGrouping`, `syncUnidentifiedGroup`) needs a list picker (owned by the pickers PR). The mapping editor's list select keeps its existing cap until that PR lands.
|
|
133
|
+
|
|
134
|
+
Slot prop `renderSubmissionActions(submission, form)`: extra buttons on an opened submission, next to its delivery state. The submission carries no contact id, so the stack pages look the submitter up by the email in `form_data` (`ContactLookupLink`) and offer "Add to contacts" when nobody has it.
|
|
@@ -66,6 +66,8 @@ import { useBrand } from "@/g8/hooks/use-brand";
|
|
|
66
66
|
export interface FormsScreenProps {
|
|
67
67
|
renderRowActions?: (form: FormListItem) => ReactNode;
|
|
68
68
|
renderDetailActions?: (form: FormListItem) => ReactNode;
|
|
69
|
+
/** Extra buttons on an opened submission, next to its delivery state (open or add the person who submitted it). */
|
|
70
|
+
renderSubmissionActions?: (submission: FormSubmission, form: FormListItem) => ReactNode;
|
|
69
71
|
extraDetailTabs?: { id: string; label: string; render: (form: FormListItem) => ReactNode }[];
|
|
70
72
|
toolbarActions?: ReactNode;
|
|
71
73
|
className?: string;
|
|
@@ -164,7 +166,7 @@ function SubmissionNotFound({ form, submissionId }: { form: FormRef; submissionI
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/** One submission in full: every submitted field, where it came from, its delivery, and the trace. */
|
|
167
|
-
function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; submission: FormSubmission; onChanged: () => void }) {
|
|
169
|
+
function SubmissionDetail({ form, submission, onChanged, actions }: { form: FormRef; submission: FormSubmission; onChanged: () => void; actions?: ReactNode }) {
|
|
168
170
|
const [retrying, setRetrying] = useState(false);
|
|
169
171
|
const trace = useG8Query(
|
|
170
172
|
(signal) => g8Get<{ capture_state?: string; receipts?: unknown[] }>(`/forms/${encodeURIComponent(form.form_key)}/submissions/${encodeURIComponent(submission.id)}/trace`, { source_id: form.source_id }, { signal }).then((r) => r.data),
|
|
@@ -180,6 +182,7 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
|
|
|
180
182
|
<StatusPill tone={failed ? "negative" : submissionState(submission) === "delivered" ? "positive" : "neutral"}>{submissionState(submission)}</StatusPill>
|
|
181
183
|
{submission.is_test ? <StatusPill tone="info">Test</StatusPill> : null}
|
|
182
184
|
{failed && submission.delivery?.receipt_id ? <Button size="sm" variant="outline" onClick={() => { retry.reset(); setRetrying(true); }}>Retry delivery</Button> : null}
|
|
185
|
+
{actions}
|
|
183
186
|
</div>
|
|
184
187
|
<SectionCard title="Submitted fields">
|
|
185
188
|
{fields.length === 0 ? <p className="text-muted-foreground text-sm">No fields were captured. Identifying fields may be redacted for your key.</p> : (
|
|
@@ -218,7 +221,7 @@ function SubmissionDetail({ form, submission, onChanged }: { form: FormRef; subm
|
|
|
218
221
|
);
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
export function FormsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
|
|
224
|
+
export function FormsScreen({ renderRowActions, renderDetailActions, renderSubmissionActions, extraDetailTabs, toolbarActions, className }: FormsScreenProps) {
|
|
222
225
|
const brand = useBrand();
|
|
223
226
|
const [tabParam, setTabParam] = useUrlState("tab", "forms");
|
|
224
227
|
const tab: Tab = tabParam === "install" || tabParam === "audit" || tabParam === "groups" ? tabParam : "forms";
|
|
@@ -369,7 +372,7 @@ export function FormsScreen({ renderRowActions, renderDetailActions, extraDetail
|
|
|
369
372
|
>
|
|
370
373
|
{selected && submissionId ? (
|
|
371
374
|
linkedSubmission ? (
|
|
372
|
-
<SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} />
|
|
375
|
+
<SubmissionDetail key={submissionId} form={selected} submission={linkedSubmission} onChanged={() => setSubsRefresh((n) => n + 1)} actions={renderSubmissionActions?.(linkedSubmission, selected)} />
|
|
373
376
|
) : lookup.error ? (
|
|
374
377
|
<ApiError error={lookup.error} title="Could not load this submission" onRetry={() => void lookup.refetch()} />
|
|
375
378
|
) : lookup.loading || !lookup.data ? (
|
package/blocks/inbox/README.md
CHANGED
|
@@ -24,6 +24,21 @@ Defaults are unchanged from this release: both are on.
|
|
|
24
24
|
|
|
25
25
|
Every detail opens as a full page with `&view=full` (the Open full page button).
|
|
26
26
|
|
|
27
|
+
**Management (new, on by default; `showManagement={false}` hides the three buttons).** The toolbar gains:
|
|
28
|
+
|
|
29
|
+
- **New message** (`ComposeMessage`): write a NEW email or text. The sender is never defaulted: you choose a mailbox
|
|
30
|
+
(from `GET /inbox/mailboxes/paginated`; ones you may not send as are listed disabled with the reason) or an SMS number,
|
|
31
|
+
and the confirm dialog names it ("from hamza@acme.com to maya@northwind.io, 2 recipients"). Sending is **external**.
|
|
32
|
+
- **Search all** (`InboxSearch`): server-side search of every email thread (across all your mailboxes) or LinkedIn
|
|
33
|
+
conversation, beyond the page the list loaded; pick a result to open it. SMS has no search route in the API. A LinkedIn result opens by the row's conversation id (`conversation_id`, else `id`); that this equals the unified inbox's LinkedIn thread id has not been checked against a live HeyReach payload, so if a result opens as "Conversation not found", the id differs.
|
|
34
|
+
- **Manage**: the tag library (`TagManager`: create, rename or recolour, delete; built-in tags follow the API's policy) and
|
|
35
|
+
`BlockedSenders` (block per mailbox, unblock). The tag list it loads also feeds the Tag filter and the tag picker.
|
|
36
|
+
- In an open conversation, **Manage conversation** (`ThreadActions`): mark read (and unread on LinkedIn), remove a tag,
|
|
37
|
+
unassign a teammate, block the sender.
|
|
38
|
+
|
|
39
|
+
A conversation this key cannot see (an org key sees shared threads only) or that was removed answers a uniform 404; the
|
|
40
|
+
conversation view says "Conversation not found" instead of showing a generic failure.
|
|
41
|
+
|
|
27
42
|
## The flow, in order
|
|
28
43
|
|
|
29
44
|
1. **List** `GET /inbox?channel=&status=&page=&page_size=` (`InboxList`). Each row has `id` and `channel`.
|
|
@@ -52,7 +67,8 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
|
|
|
52
67
|
|
|
53
68
|
- Connect a mailbox in the graph8 app (Google / Microsoft OAuth exists only there; see the `mailboxes` block).
|
|
54
69
|
Until then the inbox has no email threads. `where: "app"`.
|
|
55
|
-
- Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run` (and `campaigns:read` for the SMS sender picker
|
|
70
|
+
- Give the proxy's API key `inbox:read`, `inbox:write` and `inbox:run` (and `campaigns:read` for the SMS sender picker,
|
|
71
|
+
`inbox:delete` to delete a tag, remove a tag or teammate from a conversation or unblock a sender, `workflows:read` to list tags).
|
|
56
72
|
- Credits for AI drafts (402 when none; there is no balance read, so the confirm dialog shows no estimate).
|
|
57
73
|
|
|
58
74
|
## Defaults (the agent does not need to ask)
|
|
@@ -62,9 +78,13 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
|
|
|
62
78
|
- A reply leaves from the thread's own mailbox, number or LinkedIn account and goes to the thread's contact.
|
|
63
79
|
- No assignee filter: the API cannot filter by an arbitrary assignee email (an org key owns no threads), so
|
|
64
80
|
filter `thread.assignees` client-side if you need it.
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
- Tags: `TagManager` lists them (`GET /workflows/inbox-tags`, scope `workflows:read`), so no input is needed. Pass
|
|
82
|
+
`availableTags` only to seed the pickers before the Manage sheet is opened; tags on loaded threads are always offered.
|
|
83
|
+
- A NEW message is never sent without an explicit sender; the API refuses a compose without `from_mailbox` or
|
|
84
|
+
`from_number`, and this block does not guess one.
|
|
85
|
+
- Not offered, because the API has no such operation: snooze, archive, mark an email or SMS unread. Not offered by
|
|
86
|
+
choice (see the PR): composing a NEW LinkedIn message (`/inbox/linkedin/compose` only continues an existing
|
|
87
|
+
conversation, which `ReplyComposer` already does), starring chats, internal comments, saving compose drafts.
|
|
68
88
|
|
|
69
89
|
## Gotchas
|
|
70
90
|
|
|
@@ -88,6 +108,22 @@ IDs passed between steps: `thread.id` + `thread.channel` (list -> view -> draft/
|
|
|
88
108
|
| `aiInbox.createInboxTag` | POST /inbox/tags | write | TagAndAssign |
|
|
89
109
|
| `inbox.tagReply` | POST /inbox/{reply_id}/tag | external | TagAndAssign |
|
|
90
110
|
| `inbox.assignReply` | POST /inbox/{reply_id}/assign | external | TagAndAssign |
|
|
111
|
+
| `aiInbox.listInboxMailboxesPaginated` | GET /inbox/mailboxes/paginated | read | ComposeMessage, InboxSearch, BlockedSenders |
|
|
112
|
+
| `aiInbox.composeInboxEmail` | POST /inbox/emails/compose | external | ComposeMessage |
|
|
113
|
+
| `aiInbox.composeInboxSms` | POST /inbox/sms/compose | external | ComposeMessage |
|
|
114
|
+
| `inbox.searchInboxEmails` | POST /inbox/emails/search | read | InboxSearch |
|
|
115
|
+
| `aiInbox.searchInboxLinkedinConversations` | POST /inbox/linkedin/conversations/search | read | InboxSearch |
|
|
116
|
+
| `workflows.listInboxTags` | GET /workflows/inbox-tags | read | TagManager |
|
|
117
|
+
| `aiInbox.updateInboxTag` | PUT /inbox/tags/{tag_id} | write | TagManager (changed fields only) |
|
|
118
|
+
| `aiInbox.deleteInboxTag` | DELETE /inbox/tags/{tag_id} | destructive | TagManager |
|
|
119
|
+
| `aiInbox.markInboxEmailRead` | POST /inbox/emails/{email_id}/mark-read | write | ThreadActions |
|
|
120
|
+
| `aiInbox.markInboxSmsRead` | POST /inbox/sms/{thread_id}/mark-read | write | ThreadActions |
|
|
121
|
+
| `aiInbox.markInboxLinkedinConversationRead` / `Unread` | POST /inbox/linkedin/accounts/{account_id}/conversations/{id}/read, /unread | write | ThreadActions |
|
|
122
|
+
| `aiInbox.removeChannelTag` | DELETE /inbox/channels/{channel_id}/tags | destructive | ThreadActions |
|
|
123
|
+
| `aiInbox.unassignChannel` | DELETE /inbox/channels/{channel_id}/assign | destructive | ThreadActions |
|
|
124
|
+
| `aiInbox.blockInboxSender` | POST /inbox/blocked-senders | write | ThreadActions, BlockedSenders |
|
|
125
|
+
| `aiInbox.listInboxBlockedSenders` | GET /inbox/blocked-senders | read | BlockedSenders |
|
|
126
|
+
| `aiInbox.unblockInboxSender` | DELETE /inbox/blocked-senders/{blocked_sender_id} | destructive | BlockedSenders |
|
|
91
127
|
|
|
92
128
|
## Recipes (server side)
|
|
93
129
|
|