@manta-eu/react 0.1.6 → 0.1.8
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 -0
- package/dist/components/chart/AreaChart.d.ts.map +1 -1
- package/dist/components/chart/AreaChart.js +4 -6
- package/dist/components/chart/BarChart.d.ts.map +1 -1
- package/dist/components/chart/BarChart.js +5 -6
- package/dist/components/chart/ComposedChart.d.ts.map +1 -1
- package/dist/components/chart/ComposedChart.js +15 -7
- package/dist/components/chart/LineChart.d.ts.map +1 -1
- package/dist/components/chart/LineChart.js +4 -6
- package/dist/components/chart/annotations.d.ts +4 -0
- package/dist/components/chart/annotations.d.ts.map +1 -1
- package/dist/components/chart/annotations.js +10 -2
- package/dist/components/chart/chart-schemas.d.ts +1960 -54
- package/dist/components/chart/chart-schemas.d.ts.map +1 -1
- package/dist/components/chart/chart-schemas.js +137 -15
- package/dist/components/chart/series.d.ts +28 -0
- package/dist/components/chart/series.d.ts.map +1 -0
- package/dist/components/chart/series.js +61 -0
- package/dist/components/chart/stacks.d.ts +9 -0
- package/dist/components/chart/stacks.d.ts.map +1 -0
- package/dist/components/chart/stacks.js +11 -0
- package/dist/components/chat-view/ChatHeaders.d.ts.map +1 -1
- package/dist/components/chat-view/ChatHeaders.js +2 -2
- package/dist/components/embedded-chat/index.d.ts.map +1 -1
- package/dist/components/embedded-chat/index.js +2 -1
- package/dist/components/layouts/Sidebar.d.ts.map +1 -1
- package/dist/components/layouts/Sidebar.js +14 -3
- package/dist/components/layouts/StatusScreen.d.ts.map +1 -1
- package/dist/components/layouts/StatusScreen.js +3 -1
- package/dist/components/planning/ActioningView.d.ts.map +1 -1
- package/dist/components/planning/ActioningView.js +7 -49
- package/dist/components/planning/ExecutionSectionItem.d.ts +6 -4
- package/dist/components/planning/ExecutionSectionItem.d.ts.map +1 -1
- package/dist/components/planning/ExecutionSectionItem.js +2 -2
- package/dist/components/planning/ExecutionSections.d.ts +6 -4
- package/dist/components/planning/ExecutionSections.d.ts.map +1 -1
- package/dist/components/planning/ExecutionSections.js +2 -2
- package/dist/components/planning/ManualActionCard.d.ts +14 -8
- package/dist/components/planning/ManualActionCard.d.ts.map +1 -1
- package/dist/components/planning/ManualActionCard.js +44 -24
- package/dist/components/planning/ManualActionList.d.ts +22 -0
- package/dist/components/planning/ManualActionList.d.ts.map +1 -0
- package/dist/components/planning/ManualActionList.js +35 -0
- package/dist/components/planning/manual-action-targets.d.ts +4 -0
- package/dist/components/planning/manual-action-targets.d.ts.map +1 -0
- package/dist/components/planning/manual-action-targets.js +19 -0
- package/dist/components/sidebar/index.d.ts +2 -0
- package/dist/components/sidebar/index.d.ts.map +1 -1
- package/dist/components/sidebar/index.js +1 -0
- package/dist/components/sidebar/nav-user.d.ts +1 -1
- package/dist/components/sidebar/nav-user.d.ts.map +1 -1
- package/dist/components/sidebar/nav-user.js +3 -3
- package/dist/components/sidebar/user-avatar.d.ts +7 -0
- package/dist/components/sidebar/user-avatar.d.ts.map +1 -0
- package/dist/components/sidebar/user-avatar.js +9 -0
- package/dist/components/thinking/manta-swim.d.ts.map +1 -1
- package/dist/components/thinking/manta-swim.js +3 -1
- package/dist/globals.css +6 -13
- package/dist/hooks/use-connection-problems.d.ts.map +1 -1
- package/dist/hooks/use-manual-actions.d.ts +13 -3
- package/dist/hooks/use-manual-actions.d.ts.map +1 -1
- package/dist/hooks/use-manual-actions.js +16 -10
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +7 -1
- package/dist/hooks/useConversations.d.ts +1 -0
- package/dist/hooks/useConversations.d.ts.map +1 -1
- package/dist/hooks/useResolveManualAction.d.ts +14 -0
- package/dist/hooks/useResolveManualAction.d.ts.map +1 -0
- package/dist/hooks/useResolveManualAction.js +47 -0
- package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
- package/dist/ldo-ui.d.ts +2 -1
- package/dist/ldo-ui.d.ts.map +1 -1
- package/dist/ldo-ui.js +2 -1
- package/dist/lib/build-config.d.ts +2 -0
- package/dist/lib/build-config.d.ts.map +1 -1
- package/dist/lib/build-config.js +2 -0
- package/dist/lib/chat-types.d.ts +3 -0
- package/dist/lib/chat-types.d.ts.map +1 -1
- package/dist/lib/datastore-route-table.d.ts +2 -0
- package/dist/lib/datastore-route-table.d.ts.map +1 -1
- package/dist/lib/datastore-route-table.js +3 -1
- package/dist/lib/datastore-routes.d.ts +7 -1
- package/dist/lib/datastore-routes.d.ts.map +1 -1
- package/dist/lib/datastore-routes.js +7 -2
- package/dist/lib/decode-chart-entities.d.ts.map +1 -1
- package/dist/lib/decode-chart-entities.js +5 -1
- package/dist/lib/manual-actions.d.ts +1 -0
- package/dist/lib/manual-actions.d.ts.map +1 -1
- package/dist/lib/manual-actions.js +4 -1
- package/dist/lib/structured-data/types.d.ts +7 -2
- package/dist/lib/structured-data/types.d.ts.map +1 -1
- package/dist/lib/tool-icons.d.ts.map +1 -1
- package/dist/lib/tool-icons.js +1 -0
- package/dist/providers/AssistantIconProvider.d.ts +11 -0
- package/dist/providers/AssistantIconProvider.d.ts.map +1 -0
- package/dist/providers/AssistantIconProvider.js +10 -0
- package/dist/providers/ChatManagerProvider.d.ts.map +1 -1
- package/dist/providers/ChatManagerProvider.js +18 -16
- package/dist/providers/NavExtrasProvider.d.ts +23 -0
- package/dist/providers/NavExtrasProvider.d.ts.map +1 -0
- package/dist/providers/NavExtrasProvider.js +10 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +2 -0
- package/dist/routes/chat/index.d.ts +2 -0
- package/dist/routes/chat/index.d.ts.map +1 -1
- package/dist/routes/chat/index.js +3 -2
- package/dist/routes/explain/ExplainEmpty.d.ts.map +1 -1
- package/dist/routes/explain/ExplainEmpty.js +2 -1
- package/dist/routes/integrations/catalog-shape.d.ts +8 -3
- package/dist/routes/integrations/catalog-shape.d.ts.map +1 -1
- package/dist/routes/integrations/components/AddIntegrationModal.d.ts.map +1 -1
- package/dist/routes/integrations/components/AddIntegrationModal.js +2 -2
- package/dist/routes/integrations/components/MantaSyncPluginDownload.d.ts +13 -0
- package/dist/routes/integrations/components/MantaSyncPluginDownload.d.ts.map +1 -0
- package/dist/routes/integrations/components/MantaSyncPluginDownload.js +23 -0
- package/dist/routes/integrations/components/connect-forms/ConnectFormHeader.d.ts +1 -1
- package/dist/routes/integrations/components/connect-forms/ConnectFormHeader.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/ConnectWizardShell.d.ts +1 -1
- package/dist/routes/integrations/components/connect-forms/ConnectWizardShell.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/ConnectionStringInput.d.ts +11 -0
- package/dist/routes/integrations/components/connect-forms/ConnectionStringInput.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/ConnectionStringInput.js +49 -0
- package/dist/routes/integrations/components/connect-forms/DashConnectForm.d.ts +7 -0
- package/dist/routes/integrations/components/connect-forms/DashConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/DashConnectForm.js +87 -0
- package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.d.ts +20 -0
- package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.js +144 -0
- package/dist/routes/integrations/components/connect-forms/DataConnectionFieldInputs.d.ts +8 -0
- package/dist/routes/integrations/components/connect-forms/DataConnectionFieldInputs.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/DataConnectionFieldInputs.js +44 -0
- package/dist/routes/integrations/components/connect-forms/FigmaConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/FigmaConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/FigmaConnectForm.js +20 -0
- package/dist/routes/integrations/components/connect-forms/GmailConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/GmailConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/GmailConnectForm.js +11 -0
- package/dist/routes/integrations/components/connect-forms/GoogleAdsConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/GoogleAdsConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/GoogleAdsConnectForm.js +37 -0
- package/dist/routes/integrations/components/connect-forms/GoogleAnalyticsConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/GoogleAnalyticsConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/GoogleAnalyticsConnectForm.js +37 -0
- package/dist/routes/integrations/components/connect-forms/ImportStartedNotice.d.ts +21 -0
- package/dist/routes/integrations/components/connect-forms/ImportStartedNotice.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/ImportStartedNotice.js +25 -0
- package/dist/routes/integrations/components/connect-forms/InferalCredentialConnectForm.d.ts +45 -0
- package/dist/routes/integrations/components/connect-forms/InferalCredentialConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/InferalCredentialConnectForm.js +187 -0
- package/dist/routes/integrations/components/connect-forms/KlaviyoConnectForm.d.ts +6 -0
- package/dist/routes/integrations/components/connect-forms/KlaviyoConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/KlaviyoConnectForm.js +68 -0
- package/dist/routes/integrations/components/connect-forms/LinkedinConnectForm.d.ts +10 -0
- package/dist/routes/integrations/components/connect-forms/LinkedinConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/LinkedinConnectForm.js +124 -0
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.d.ts +14 -0
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.js +183 -0
- package/dist/routes/integrations/components/connect-forms/OAuthConnectLink.d.ts +10 -0
- package/dist/routes/integrations/components/connect-forms/OAuthConnectLink.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/OAuthConnectLink.js +17 -0
- package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.d.ts +45 -0
- package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js +136 -0
- package/dist/routes/integrations/components/connect-forms/OAuthSingleAccountConnectForm.d.ts +23 -0
- package/dist/routes/integrations/components/connect-forms/OAuthSingleAccountConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/OAuthSingleAccountConnectForm.js +63 -0
- package/dist/routes/integrations/components/connect-forms/ShopifyConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/ShopifyConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/ShopifyConnectForm.js +14 -0
- package/dist/routes/integrations/components/connect-forms/SlackConnectForm.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/SlackConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/SlackConnectForm.js +10 -0
- package/dist/routes/integrations/components/connect-forms/TriplewhaleConnectForm.d.ts +5 -0
- package/dist/routes/integrations/components/connect-forms/TriplewhaleConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/TriplewhaleConnectForm.js +35 -0
- package/dist/routes/integrations/components/connect-forms/WoocommerceConnectForm.d.ts +5 -0
- package/dist/routes/integrations/components/connect-forms/WoocommerceConnectForm.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/WoocommerceConnectForm.js +47 -0
- package/dist/routes/integrations/components/connect-forms/connection-string-fields.d.ts +13 -0
- package/dist/routes/integrations/components/connect-forms/connection-string-fields.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/connection-string-fields.js +110 -0
- package/dist/routes/integrations/components/connect-forms/dash-folder-tree.d.ts +6 -0
- package/dist/routes/integrations/components/connect-forms/dash-folder-tree.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/dash-folder-tree.js +14 -0
- package/dist/routes/integrations/components/connect-forms/data-connection-fields.d.ts +35 -0
- package/dist/routes/integrations/components/connect-forms/data-connection-fields.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/data-connection-fields.js +88 -0
- package/dist/routes/integrations/components/connect-forms/google-ads-account-tree.d.ts +11 -0
- package/dist/routes/integrations/components/connect-forms/google-ads-account-tree.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/google-ads-account-tree.js +36 -0
- package/dist/routes/integrations/components/connect-forms/google-analytics-property-tree.d.ts +6 -0
- package/dist/routes/integrations/components/connect-forms/google-analytics-property-tree.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/google-analytics-property-tree.js +34 -0
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.d.ts +12 -0
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.js +33 -0
- package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.d.ts +3 -0
- package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/setup-account-unavailable-reason.js +8 -0
- package/dist/routes/integrations/components/connect-forms/types.d.ts +6 -1
- package/dist/routes/integrations/components/connect-forms/types.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/useConnectWizard.d.ts +19 -0
- package/dist/routes/integrations/components/connect-forms/useConnectWizard.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/useConnectWizard.js +28 -0
- package/dist/routes/integrations/components/connect-forms/useConnectedExternalIds.d.ts +4 -0
- package/dist/routes/integrations/components/connect-forms/useConnectedExternalIds.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/useConnectedExternalIds.js +17 -0
- package/dist/routes/integrations/components/connect-forms/useWindowFocusPoll.d.ts +7 -0
- package/dist/routes/integrations/components/connect-forms/useWindowFocusPoll.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/useWindowFocusPoll.js +18 -0
- package/dist/routes/integrations/connect-forms.d.ts +16 -0
- package/dist/routes/integrations/connect-forms.d.ts.map +1 -0
- package/dist/routes/integrations/connect-forms.js +14 -0
- package/dist/routes/integrations/connection-view.d.ts.map +1 -1
- package/dist/routes/integrations/connection-view.js +30 -7
- package/dist/routes/integrations/filter-connected-providers.d.ts +3 -0
- package/dist/routes/integrations/filter-connected-providers.d.ts.map +1 -0
- package/dist/routes/integrations/filter-connected-providers.js +15 -0
- package/dist/routes/integrations/overview.d.ts +3 -0
- package/dist/routes/integrations/overview.d.ts.map +1 -0
- package/dist/routes/integrations/overview.js +2 -0
- package/dist/routes/integrations/types.d.ts +24 -4
- package/dist/routes/integrations/types.d.ts.map +1 -1
- package/dist/routes/integrations/types.js +10 -0
- package/dist/routes/integrations/use-connected-account-entities.d.ts +10 -0
- package/dist/routes/integrations/use-connected-account-entities.d.ts.map +1 -0
- package/dist/routes/integrations/use-connected-account-entities.js +29 -0
- package/dist/routes/memories/components/EmptyStates.d.ts.map +1 -1
- package/dist/routes/memories/components/EmptyStates.js +2 -1
- package/dist/routes/plans/components/actionPlansTableConfig.d.ts +12 -12
- package/package.json +54 -10
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
3
|
+
import { GQLInferalLocation, useCreateInferalIntegrationMutation, } from "@manta-eu/client";
|
|
4
|
+
import { parseError } from "@manta-eu/client/shared/error-utils";
|
|
5
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
6
|
+
import { Fragment, useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { Controller, useForm } from "react-hook-form";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { ConnectWizardShell } from "./ConnectWizardShell.js";
|
|
11
|
+
import { FieldInstructions } from "./FieldInstructions.js";
|
|
12
|
+
import { ImportStartedStep } from "./ImportStartedNotice.js";
|
|
13
|
+
import { useConnectWizard } from "./useConnectWizard.js";
|
|
14
|
+
import { Input } from "#components/input/index";
|
|
15
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "#components/select/index";
|
|
16
|
+
import { useMantaClient } from "#providers/index";
|
|
17
|
+
const locationSchema = z.enum(GQLInferalLocation, { message: "Region is required" });
|
|
18
|
+
// Kebab-casing the field key keeps the rendered input ids identical to the hand-written forms.
|
|
19
|
+
export function fieldId(prefix, key) {
|
|
20
|
+
return `${prefix}-${key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
21
|
+
}
|
|
22
|
+
const streamNameSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.trim()
|
|
25
|
+
.min(1, "Stream name is required")
|
|
26
|
+
.max(64, "64 characters or fewer")
|
|
27
|
+
.regex(/^[a-zA-Z0-9_:-]+$/, "Use letters, numbers, dashes (-), underscores (_), or colons (:). No spaces.");
|
|
28
|
+
function blankValues(fields) {
|
|
29
|
+
return { ...Object.fromEntries(fields.map((field) => [field.key, ""])), streamName: "" };
|
|
30
|
+
}
|
|
31
|
+
// The stream name tracks the identifier only until the user types their own.
|
|
32
|
+
export function shouldAutofillStreamName(changedField, identifierKey, streamNameDirty) {
|
|
33
|
+
return changedField === identifierKey && !streamNameDirty;
|
|
34
|
+
}
|
|
35
|
+
function credentialSchema(fields) {
|
|
36
|
+
return z.object(Object.fromEntries(fields.map((f) => [f.key, credentialFieldSchema(f)])));
|
|
37
|
+
}
|
|
38
|
+
function credentialFieldSchema(field) {
|
|
39
|
+
const required = z.string().trim().min(1, field.requiredMessage);
|
|
40
|
+
const { validate } = field;
|
|
41
|
+
return validate ? required.refine(validate.check, validate.message) : required;
|
|
42
|
+
}
|
|
43
|
+
// The credential fields are only known at runtime, so the built schema carries no usable output
|
|
44
|
+
// type; naming it keeps `useForm` inferring the same Values the hand-written forms declared.
|
|
45
|
+
// A schema built from runtime descriptors has no usable static output type, so this one assertion
|
|
46
|
+
// stands in for it; every other type in the file is inferred from the caller's key union.
|
|
47
|
+
function formSchema(credentialFields) {
|
|
48
|
+
return credentialSchema(credentialFields).extend({
|
|
49
|
+
streamName: streamNameSchema,
|
|
50
|
+
location: locationSchema,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Object.fromEntries cannot carry the key union through, so the completeness check below is what
|
|
54
|
+
// makes the assertion true rather than merely asserted.
|
|
55
|
+
function pickCredentials(values, fields) {
|
|
56
|
+
const missing = fields.filter((field) => !isPresent(values[field.key])).map((field) => field.key);
|
|
57
|
+
if (isPresent(missing)) {
|
|
58
|
+
throw new Error(`Missing credential ${missing.join(", ")} — the form declared it but collected no value`);
|
|
59
|
+
}
|
|
60
|
+
return Object.fromEntries(fields.map((field) => [field.key, values[field.key]]));
|
|
61
|
+
}
|
|
62
|
+
function renderField(form, prefix, field) {
|
|
63
|
+
return (_jsx(Input, { id: fieldId(prefix, field.key), label: field.label, type: field.secret ? "password" : undefined, placeholder: field.placeholder, autoComplete: "off", error: form.formState.errors[field.key]?.message?.toString(), ...form.register(field.key) }, field.key));
|
|
64
|
+
}
|
|
65
|
+
function renderFields(form, prefix, fields) {
|
|
66
|
+
const [only] = fields;
|
|
67
|
+
if (fields.length === 1 && only)
|
|
68
|
+
return renderField(form, prefix, only);
|
|
69
|
+
return _jsx("div", { className: "grid gap-3", children: fields.map((field) => renderField(form, prefix, field)) });
|
|
70
|
+
}
|
|
71
|
+
export function createSteps(config, locationLabels, setup) {
|
|
72
|
+
const { fieldIdPrefix, identifier, secrets, settings } = config;
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
id: "identifier",
|
|
76
|
+
title: identifier.stepTitle,
|
|
77
|
+
description: identifier.stepDescription,
|
|
78
|
+
schema: credentialSchema([identifier]),
|
|
79
|
+
supporting: setup?.identifier ? _jsx(FieldInstructions, { instruction: setup.identifier }) : undefined,
|
|
80
|
+
continueLabel: "Continue",
|
|
81
|
+
render: (form) => renderField(form, fieldIdPrefix, identifier),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "secrets",
|
|
85
|
+
title: secrets.title,
|
|
86
|
+
description: secrets.description,
|
|
87
|
+
schema: credentialSchema(secrets.fields),
|
|
88
|
+
supporting: setup?.apiKey ? _jsx(FieldInstructions, { instruction: setup.apiKey }) : undefined,
|
|
89
|
+
continueLabel: "Continue",
|
|
90
|
+
render: (form) => renderFields(form, fieldIdPrefix, secrets.fields),
|
|
91
|
+
},
|
|
92
|
+
...(settings
|
|
93
|
+
? [
|
|
94
|
+
{
|
|
95
|
+
id: "settings",
|
|
96
|
+
title: settings.title,
|
|
97
|
+
description: settings.description,
|
|
98
|
+
schema: credentialSchema(settings.fields),
|
|
99
|
+
supporting: setup?.settings ? _jsx(FieldInstructions, { instruction: setup.settings }) : undefined,
|
|
100
|
+
continueLabel: "Continue",
|
|
101
|
+
render: (form) => renderFields(form, fieldIdPrefix, settings.fields),
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
{
|
|
106
|
+
id: "stream-name",
|
|
107
|
+
title: "Name this integration",
|
|
108
|
+
description: "Used to identify this connection. Letters, numbers, dashes, and underscores.",
|
|
109
|
+
schema: z.object({ streamName: streamNameSchema }),
|
|
110
|
+
continueLabel: "Continue",
|
|
111
|
+
render: (form) => (_jsx(Input, { id: fieldId(fieldIdPrefix, "streamName"), label: "Stream name", placeholder: config.streamNamePlaceholder, autoComplete: "off", error: form.formState.errors.streamName?.message?.toString(), ...form.register("streamName") })),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "location",
|
|
115
|
+
title: "Choose a stream location",
|
|
116
|
+
description: config.locationStepDescription,
|
|
117
|
+
schema: z.object({ location: locationSchema }),
|
|
118
|
+
continueLabel: "Connect",
|
|
119
|
+
render: (form) => (_jsx(Controller, { control: form.control, name: "location", render: ({ field, fieldState }) => (_jsxs(Select, { value: field.value ?? "", onValueChange: field.onChange, children: [_jsx(SelectTrigger, { label: "Location", id: fieldId(fieldIdPrefix, "location"), className: "w-full h-10", error: fieldState.error?.message, children: _jsx(SelectValue, { placeholder: "Select a region" }) }), _jsx(SelectContent, { children: Object.entries(locationLabels).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value))) })] })) })),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
export function InferalCredentialConnectForm(props) {
|
|
124
|
+
const { onBack, onChanged, onDismiss, provider, inferalConfig, config, importNotice } = props;
|
|
125
|
+
if (!inferalConfig) {
|
|
126
|
+
throw new Error(`${config.providerName} is missing its Inferal connection configuration`);
|
|
127
|
+
}
|
|
128
|
+
const { deriveStreamName, locationLabels, setupInstructions } = inferalConfig;
|
|
129
|
+
const { providerName, identifier, secrets, settings, buildProviderInput } = config;
|
|
130
|
+
const credentialFields = useMemo(() => [identifier, ...secrets.fields, ...(settings?.fields ?? [])], [identifier, secrets, settings]);
|
|
131
|
+
const steps = useMemo(() => createSteps(config, locationLabels, setupInstructions), [config, locationLabels, setupInstructions]);
|
|
132
|
+
const fullSchema = useMemo(() => formSchema(credentialFields), [credentialFields]);
|
|
133
|
+
const { mantaClient } = useMantaClient();
|
|
134
|
+
const createMutation = useCreateInferalIntegrationMutation(mantaClient);
|
|
135
|
+
const [connected, setConnected] = useState(false);
|
|
136
|
+
const form = useForm({
|
|
137
|
+
resolver: zodResolver(fullSchema),
|
|
138
|
+
defaultValues: blankValues(credentialFields),
|
|
139
|
+
mode: "onChange",
|
|
140
|
+
});
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
const subscription = form.watch((values, { name }) => {
|
|
143
|
+
if (!shouldAutofillStreamName(name, identifier.key, Boolean(form.formState.dirtyFields.streamName)))
|
|
144
|
+
return;
|
|
145
|
+
form.setValue("streamName", deriveStreamName(values[identifier.key] ?? ""));
|
|
146
|
+
});
|
|
147
|
+
return () => subscription.unsubscribe();
|
|
148
|
+
}, [deriveStreamName, form, identifier.key]);
|
|
149
|
+
async function onValid(values) {
|
|
150
|
+
const credentials = pickCredentials(values, credentialFields);
|
|
151
|
+
try {
|
|
152
|
+
const response = await createMutation.mutateAsync({
|
|
153
|
+
input: {
|
|
154
|
+
provider: buildProviderInput(credentials),
|
|
155
|
+
stream: { name: values.streamName, location: values.location },
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const result = response.createInferalIntegration;
|
|
159
|
+
onChanged();
|
|
160
|
+
if (isPresent(result.error)) {
|
|
161
|
+
toast.error(`Failed to connect ${providerName}`, { description: result.error });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
toast.success(`${providerName} connected`, { description: values.streamName });
|
|
165
|
+
if (importNotice)
|
|
166
|
+
setConnected(true);
|
|
167
|
+
else
|
|
168
|
+
onDismiss();
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
onChanged();
|
|
172
|
+
const info = parseError(err, `Failed to connect ${providerName}`);
|
|
173
|
+
toast.error(info.message, { description: info.description });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const { stepIndex, currentStep, canAdvance, handleContinue, handleBack } = useConnectWizard({
|
|
177
|
+
steps,
|
|
178
|
+
form,
|
|
179
|
+
fullSchema,
|
|
180
|
+
onSubmit: onValid,
|
|
181
|
+
onBack,
|
|
182
|
+
});
|
|
183
|
+
if (connected && importNotice) {
|
|
184
|
+
return (_jsx(ImportStartedStep, { provider: provider, notice: importNotice, totalSteps: steps.length + 1, onDone: onDismiss }));
|
|
185
|
+
}
|
|
186
|
+
return (_jsx(ConnectWizardShell, { provider: provider, totalSteps: importNotice ? steps.length + 1 : steps.length, currentStep: stepIndex + 1, stepTitle: currentStep.title, stepDescription: currentStep.description, supportingInfo: currentStep.supporting, onBack: handleBack, onContinue: handleContinue, continueLabel: currentStep.continueLabel, continueDisabled: !canAdvance, isSubmitting: createMutation.isPending, children: _jsx(Fragment, { children: currentStep.render(form) }, currentStep.id) }));
|
|
187
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type InferalCredentialFormConfig } from "./InferalCredentialConnectForm.js";
|
|
2
|
+
import type { ConnectFormProps } from "./types.js";
|
|
3
|
+
export declare function extractKlaviyoMetricId(value: string): string;
|
|
4
|
+
export declare const klaviyoConfig: InferalCredentialFormConfig<"companyId" | "apiKey" | "conversionMetricId">;
|
|
5
|
+
export declare function KlaviyoConnectForm(props: ConnectFormProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=KlaviyoConnectForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KlaviyoConnectForm.d.ts","sourceRoot":"","sources":["../../../../../src/routes/integrations/components/connect-forms/KlaviyoConnectForm.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAChH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAIhD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK5D;AASD,eAAO,MAAM,aAAa,EAAE,2BAA2B,CAAC,WAAW,GAAG,QAAQ,GAAG,oBAAoB,CAkDpG,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,2CAEzD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { InferalCredentialConnectForm } from "./InferalCredentialConnectForm.js";
|
|
3
|
+
// The setup steps send the user to klaviyo.com/metric/<ID>/<name>, so the whole URL is the likeliest
|
|
4
|
+
// paste. Keep the ID and drop the rest rather than handing the relay something it cannot use.
|
|
5
|
+
export function extractKlaviyoMetricId(value) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
const fromUrl = /\/metric\/([^/?#]+)/.exec(trimmed);
|
|
8
|
+
return fromUrl?.[1] ?? trimmed;
|
|
9
|
+
}
|
|
10
|
+
// Klaviyo publishes no format for a metric ID — its own schema types one as an unconstrained string —
|
|
11
|
+
// so this rejects only what an ID certainly is not: a leftover path, a query string, or a stray word.
|
|
12
|
+
function looksLikeMetricId(value) {
|
|
13
|
+
return /^[A-Za-z0-9_-]+$/.test(extractKlaviyoMetricId(value));
|
|
14
|
+
}
|
|
15
|
+
// companyId identifies the integration on our side only — Klaviyo's relay contract never receives it.
|
|
16
|
+
export const klaviyoConfig = {
|
|
17
|
+
providerName: "Klaviyo",
|
|
18
|
+
fieldIdPrefix: "klaviyo",
|
|
19
|
+
identifier: {
|
|
20
|
+
key: "companyId",
|
|
21
|
+
label: "Company ID",
|
|
22
|
+
placeholder: "Your Klaviyo company ID",
|
|
23
|
+
requiredMessage: "Company ID is required",
|
|
24
|
+
stepTitle: "Find your Klaviyo company ID",
|
|
25
|
+
stepDescription: "We use this to name the stream, so it is unique to this Klaviyo account.",
|
|
26
|
+
},
|
|
27
|
+
secrets: {
|
|
28
|
+
title: "Create a private API key",
|
|
29
|
+
description: "Manta needs read access to campaigns, flows, profiles, lists, and segments.",
|
|
30
|
+
fields: [
|
|
31
|
+
{
|
|
32
|
+
key: "apiKey",
|
|
33
|
+
label: "Private API key",
|
|
34
|
+
placeholder: "pk_...",
|
|
35
|
+
requiredMessage: "Private API key is required",
|
|
36
|
+
secret: true,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
settings: {
|
|
41
|
+
title: "Choose your conversion metric",
|
|
42
|
+
description: "Klaviyo attributes campaign and flow revenue to one metric of your choosing, and picks none by default.",
|
|
43
|
+
fields: [
|
|
44
|
+
{
|
|
45
|
+
key: "conversionMetricId",
|
|
46
|
+
label: "Conversion metric ID",
|
|
47
|
+
placeholder: "RESQ6t",
|
|
48
|
+
requiredMessage: "Conversion metric ID is required",
|
|
49
|
+
validate: {
|
|
50
|
+
check: looksLikeMetricId,
|
|
51
|
+
message: "Paste the metric ID, or the whole klaviyo.com/metric/... URL",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
streamNamePlaceholder: "klaviyo-abc123",
|
|
57
|
+
locationStepDescription: "Pick the region closest to your account for the lowest latency.",
|
|
58
|
+
buildProviderInput: (credentials) => ({
|
|
59
|
+
klaviyo: {
|
|
60
|
+
companyId: credentials.companyId,
|
|
61
|
+
apiKey: credentials.apiKey,
|
|
62
|
+
conversionMetricId: extractKlaviyoMetricId(credentials.conversionMetricId),
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
export function KlaviyoConnectForm(props) {
|
|
67
|
+
return _jsx(InferalCredentialConnectForm, { ...props, config: klaviyoConfig });
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConnectFormProps } from "./types.js";
|
|
2
|
+
type LinkedinPhase = "intro" | "waiting" | "selecting" | "finished" | "failed";
|
|
3
|
+
type DeriveLinkedinPhaseInput = {
|
|
4
|
+
states?: string[];
|
|
5
|
+
hasSeenActiveState: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function deriveLinkedinPhase({ states, hasSeenActiveState }: DeriveLinkedinPhaseInput): LinkedinPhase;
|
|
8
|
+
export declare function LinkedinConnectForm(props: ConnectFormProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=LinkedinConnectForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LinkedinConnectForm.d.ts","sourceRoot":"","sources":["../../../../../src/routes/integrations/components/connect-forms/LinkedinConnectForm.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAchD,KAAK,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE/E,KAAK,wBAAwB,GAAG;IAC9B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAOF,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,wBAAwB,GAAG,aAAa,CAe3G;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,2CA4K1D"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLinkedinSetupConnectMutation, useLinkedinSetupProgressQuery } from "@manta-eu/client";
|
|
3
|
+
import { parseError } from "@manta-eu/client/shared/error-utils";
|
|
4
|
+
import { humanCount } from "@manta-eu/lib/humanize";
|
|
5
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
6
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { ArrowLeft } from "lucide-react";
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
import { match } from "ts-pattern";
|
|
11
|
+
import { ConnectFormHeader } from "./ConnectFormHeader.js";
|
|
12
|
+
import { ImportStartedNotice } from "./ImportStartedNotice.js";
|
|
13
|
+
import { OAuthConnectLink } from "./OAuthConnectLink.js";
|
|
14
|
+
import { useWindowFocusPoll } from "./useWindowFocusPoll.js";
|
|
15
|
+
import { Banner } from "#components/banner/index";
|
|
16
|
+
import { Button } from "#components/button/index";
|
|
17
|
+
import { Checkbox } from "#components/checkbox/index";
|
|
18
|
+
import { Loader } from "#components/loader/index";
|
|
19
|
+
import { useMantaClient } from "#providers/index";
|
|
20
|
+
const selectingState = "SELECTING_ACCOUNTS";
|
|
21
|
+
const finishedState = "FINISHED";
|
|
22
|
+
const notFoundState = "NOT_FOUND";
|
|
23
|
+
const failedStates = new Set(["FATALLY_FAILED", "TIMED_OUT"]);
|
|
24
|
+
const pollIntervalMs = 4000;
|
|
25
|
+
function isActiveState(states) {
|
|
26
|
+
return (states ?? []).some((state) => state !== finishedState && state !== notFoundState && !failedStates.has(state));
|
|
27
|
+
}
|
|
28
|
+
// hasSeenActiveState blocks acting on leftover terminal progress from a past session.
|
|
29
|
+
export function deriveLinkedinPhase({ states, hasSeenActiveState }) {
|
|
30
|
+
const set = new Set(states ?? []);
|
|
31
|
+
return match({
|
|
32
|
+
selecting: set.has(selectingState),
|
|
33
|
+
finished: hasSeenActiveState && set.has(finishedState),
|
|
34
|
+
failed: hasSeenActiveState && [...failedStates].some((value) => set.has(value)),
|
|
35
|
+
active: isActiveState(states),
|
|
36
|
+
})
|
|
37
|
+
.returnType()
|
|
38
|
+
.with({ selecting: true }, () => "selecting")
|
|
39
|
+
.with({ finished: true }, () => "finished")
|
|
40
|
+
.with({ failed: true }, () => "failed")
|
|
41
|
+
.with({ active: true }, () => "waiting")
|
|
42
|
+
.otherwise(() => "intro");
|
|
43
|
+
}
|
|
44
|
+
export function LinkedinConnectForm(props) {
|
|
45
|
+
const { onBack, onChanged, onDismiss, provider, resumedFromOAuth = false, importNotice } = props;
|
|
46
|
+
const { mantaClient } = useMantaClient();
|
|
47
|
+
const queryClient = useQueryClient();
|
|
48
|
+
const [hasSeenActiveState, setHasSeenActiveState] = useState(resumedFromOAuth);
|
|
49
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
50
|
+
const hasReportedConnected = useRef(false);
|
|
51
|
+
const connect = useLinkedinSetupConnectMutation(mantaClient);
|
|
52
|
+
const progressQuery = useLinkedinSetupProgressQuery(mantaClient, undefined, { staleTime: 0 });
|
|
53
|
+
const progress = progressQuery.data?.linkedinSetupProgress;
|
|
54
|
+
const accounts = progress?.accounts ?? [];
|
|
55
|
+
const phase = deriveLinkedinPhase({ states: progress?.state, hasSeenActiveState });
|
|
56
|
+
useWindowFocusPoll(progressQuery.refetch, {
|
|
57
|
+
enabled: phase === "waiting",
|
|
58
|
+
intervalMs: pollIntervalMs,
|
|
59
|
+
});
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (isActiveState(progress?.state))
|
|
62
|
+
setHasSeenActiveState(true);
|
|
63
|
+
}, [progress]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (phase === "finished" && !hasReportedConnected.current) {
|
|
66
|
+
hasReportedConnected.current = true;
|
|
67
|
+
onChanged();
|
|
68
|
+
toast.success("LinkedIn Ads connected");
|
|
69
|
+
}
|
|
70
|
+
}, [phase, onChanged]);
|
|
71
|
+
function resetProgress() {
|
|
72
|
+
hasReportedConnected.current = false;
|
|
73
|
+
setSelectedIds(new Set());
|
|
74
|
+
queryClient.removeQueries({ queryKey: useLinkedinSetupProgressQuery.getKey() });
|
|
75
|
+
}
|
|
76
|
+
function handleOpened() {
|
|
77
|
+
resetProgress();
|
|
78
|
+
setHasSeenActiveState(false);
|
|
79
|
+
}
|
|
80
|
+
function handleReset() {
|
|
81
|
+
resetProgress();
|
|
82
|
+
setHasSeenActiveState(false);
|
|
83
|
+
}
|
|
84
|
+
function toggleAccount(id, checked) {
|
|
85
|
+
setSelectedIds((current) => {
|
|
86
|
+
const next = new Set(current);
|
|
87
|
+
if (checked)
|
|
88
|
+
next.add(id);
|
|
89
|
+
else
|
|
90
|
+
next.delete(id);
|
|
91
|
+
return next;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function submitAccounts() {
|
|
95
|
+
try {
|
|
96
|
+
await connect.mutateAsync({ accountIds: [...selectedIds] });
|
|
97
|
+
void progressQuery.refetch();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const info = parseError(err, "Couldn't connect the selected accounts");
|
|
101
|
+
toast.error(info.message, { description: info.description });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function renderBody() {
|
|
105
|
+
if (phase === "failed") {
|
|
106
|
+
return (_jsx(FailedStep, { message: progress?.lastError?.message ?? "We couldn't finish connecting LinkedIn. Please try again.", title: progress?.lastError?.title, onRetry: handleReset }));
|
|
107
|
+
}
|
|
108
|
+
if (phase === "finished") {
|
|
109
|
+
return (_jsxs("div", { className: "grid gap-4", children: [importNotice ? (_jsx(ImportStartedNotice, { providerName: provider.name, notice: importNotice })) : (_jsx("p", { className: "typography-caption text-muted-foreground", children: "Your LinkedIn Ads accounts are connected." })), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: onDismiss, children: "Done" }) })] }));
|
|
110
|
+
}
|
|
111
|
+
if (phase === "selecting") {
|
|
112
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsx("p", { className: "typography-caption text-muted-foreground", children: "Choose the LinkedIn Ads accounts Manta should connect." }), _jsx("div", { className: "grid gap-2 max-h-72 overflow-y-auto", children: isPresent(accounts) ? (accounts.map((account) => (_jsx(Checkbox, { checked: selectedIds.has(account.id), onCheckedChange: (checked) => toggleAccount(account.id, checked === true), label: account.name }, account.id)))) : (_jsx("p", { className: "typography-caption text-muted-foreground", children: "No ad accounts were found for this LinkedIn login." })) }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx(Button, { variant: "tertiary", onClick: onBack, icon: ArrowLeft, children: "Back" }), _jsxs(Button, { onClick: () => void submitAccounts(), disabled: selectedIds.size === 0, isLoading: connect.isPending, children: ["Connect ", humanCount(selectedIds.size, "account")] })] })] }));
|
|
113
|
+
}
|
|
114
|
+
if (phase === "waiting") {
|
|
115
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "flex items-center gap-3", role: "status", "aria-live": "polite", children: [_jsx(Loader, {}), _jsx("p", { className: "typography-caption text-muted-foreground", children: "Loading your LinkedIn ad accounts\u2026" })] }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx(Button, { variant: "tertiary", onClick: onBack, icon: ArrowLeft, children: "Back" }), _jsx(Button, { variant: "secondary", onClick: handleReset, children: "Start over" })] })] }));
|
|
116
|
+
}
|
|
117
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsx("p", { className: "typography-caption text-muted-foreground", children: "Sign in with LinkedIn to let Manta read your Ads performance. You'll be taken to LinkedIn to authorize access." }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx(Button, { variant: "tertiary", onClick: onBack, icon: ArrowLeft, children: "Back" }), _jsx(OAuthConnectLink, { href: "/oauth/linkedin/start", label: "Continue with LinkedIn", onOpened: handleOpened })] })] }));
|
|
118
|
+
}
|
|
119
|
+
return (_jsxs("div", { className: "grid gap-6", children: [_jsx(ConnectFormHeader, { provider: provider, status: phase === "finished" ? "Connected" : "Not connected" }), renderBody()] }));
|
|
120
|
+
}
|
|
121
|
+
function FailedStep(props) {
|
|
122
|
+
const { message, title, onRetry } = props;
|
|
123
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsx(Banner, { type: "error", size: "loose", label: title ?? "LinkedIn connection failed", description: message }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { onClick: onRetry, children: "Try again" }) })] }));
|
|
124
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GQLMetaSetupStateEnum, type GQLMetaAdAccount } from "@manta-eu/client";
|
|
2
|
+
import type { ConnectFormProps } from "./types.js";
|
|
3
|
+
type Phase = "idle" | "awaitingOAuth" | "loadingAccounts" | "pickingAccount" | "submitting" | "done" | "failed";
|
|
4
|
+
type DerivePhaseInput = {
|
|
5
|
+
states: GQLMetaSetupStateEnum[] | undefined;
|
|
6
|
+
hasStartedOAuth: boolean;
|
|
7
|
+
hasSeenActiveState: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function derivePhase({ states, hasStartedOAuth, hasSeenActiveState }: DerivePhaseInput): Phase;
|
|
10
|
+
export declare function isSelectableAccount(account: GQLMetaAdAccount, alreadyConnectedExternalIds: ReadonlySet<string>, reconnectableExternalIds?: ReadonlySet<string>): boolean;
|
|
11
|
+
export declare function accountHint(account: GQLMetaAdAccount, alreadyConnectedExternalIds: ReadonlySet<string>, reconnectableExternalIds?: ReadonlySet<string>): string | undefined;
|
|
12
|
+
export declare function MetaAdsConnectForm(props: ConnectFormProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=MetaAdsConnectForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MetaAdsConnectForm.d.ts","sourceRoot":"","sources":["../../../../../src/routes/integrations/components/connect-forms/MetaAdsConnectForm.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,qBAAqB,EAIrB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAY1B,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,SAAS,CAAC;AAY9D,KAAK,KAAK,GAAG,MAAM,GAAG,eAAe,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEhH,KAAK,gBAAgB,GAAG;IACtB,MAAM,EAAE,qBAAqB,EAAE,GAAG,SAAS,CAAC;IAC5C,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAoBF,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,eAAe,EAAE,kBAAkB,EAAE,EAAE,gBAAgB,GAAG,KAAK,CAYpG;AAkBD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,gBAAgB,EACzB,2BAA2B,EAAE,WAAW,CAAC,MAAM,CAAC,EAChD,wBAAwB,GAAE,WAAW,CAAC,MAAM,CAAa,GACxD,OAAO,CAKT;AAED,wBAAgB,WAAW,CACzB,OAAO,EAAE,gBAAgB,EACzB,2BAA2B,EAAE,WAAW,CAAC,MAAM,CAAC,EAChD,wBAAwB,GAAE,WAAW,CAAC,MAAM,CAAa,GACxD,MAAM,GAAG,SAAS,CAKpB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,gBAAgB,2CA6HzD"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { GQLIntegration, GQLMetaSetupStateEnum, useConnectedAccountsQuery, useMetaSetupConnectMutation, useMetaSetupProgressQuery, } from "@manta-eu/client";
|
|
3
|
+
import { parseError } from "@manta-eu/client/shared/error-utils";
|
|
4
|
+
import { isPresent } from "@manta-eu/lib/present";
|
|
5
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
+
import { ArrowLeft } from "lucide-react";
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { toast } from "sonner";
|
|
9
|
+
import { match } from "ts-pattern";
|
|
10
|
+
import { ConnectFormHeader } from "./ConnectFormHeader.js";
|
|
11
|
+
import { ImportStartedNotice } from "./ImportStartedNotice.js";
|
|
12
|
+
import { OAuthConnectLink } from "./OAuthConnectLink.js";
|
|
13
|
+
import { unavailableReasonLabel } from "./setup-account-unavailable-reason.js";
|
|
14
|
+
import { useReconnectableExternalIds } from "./useConnectedExternalIds.js";
|
|
15
|
+
import { useWindowFocusPoll } from "./useWindowFocusPoll.js";
|
|
16
|
+
import { Banner } from "#components/banner/index";
|
|
17
|
+
import { Button } from "#components/button/index";
|
|
18
|
+
import { Loader } from "#components/loader/index";
|
|
19
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "#components/select/index";
|
|
20
|
+
import { cn } from "#lib/utils";
|
|
21
|
+
import { useMantaClient } from "#providers/index";
|
|
22
|
+
const pollIntervalMs = 3000;
|
|
23
|
+
const statePriority = [
|
|
24
|
+
GQLMetaSetupStateEnum.Finished,
|
|
25
|
+
GQLMetaSetupStateEnum.FatallyFailed,
|
|
26
|
+
GQLMetaSetupStateEnum.TimedOut,
|
|
27
|
+
GQLMetaSetupStateEnum.ConnectingAccounts,
|
|
28
|
+
GQLMetaSetupStateEnum.SelectingAccounts,
|
|
29
|
+
GQLMetaSetupStateEnum.FetchingAccounts,
|
|
30
|
+
GQLMetaSetupStateEnum.Setup,
|
|
31
|
+
];
|
|
32
|
+
const terminalStates = [
|
|
33
|
+
GQLMetaSetupStateEnum.Finished,
|
|
34
|
+
GQLMetaSetupStateEnum.FatallyFailed,
|
|
35
|
+
GQLMetaSetupStateEnum.TimedOut,
|
|
36
|
+
];
|
|
37
|
+
const activePhases = ["awaitingOAuth", "loadingAccounts", "pickingAccount", "submitting"];
|
|
38
|
+
export function derivePhase({ states, hasStartedOAuth, hasSeenActiveState }) {
|
|
39
|
+
const defaultPhase = hasStartedOAuth ? "awaitingOAuth" : "idle";
|
|
40
|
+
const dominant = statePriority.find((state) => states?.includes(state));
|
|
41
|
+
if (dominant && terminalStates.includes(dominant) && !hasSeenActiveState)
|
|
42
|
+
return defaultPhase;
|
|
43
|
+
return match(dominant)
|
|
44
|
+
.returnType()
|
|
45
|
+
.with(GQLMetaSetupStateEnum.Finished, () => "done")
|
|
46
|
+
.with(GQLMetaSetupStateEnum.FatallyFailed, GQLMetaSetupStateEnum.TimedOut, () => "failed")
|
|
47
|
+
.with(GQLMetaSetupStateEnum.ConnectingAccounts, () => "submitting")
|
|
48
|
+
.with(GQLMetaSetupStateEnum.SelectingAccounts, () => "pickingAccount")
|
|
49
|
+
.with(GQLMetaSetupStateEnum.FetchingAccounts, GQLMetaSetupStateEnum.Setup, () => "loadingAccounts")
|
|
50
|
+
.otherwise(() => defaultPhase);
|
|
51
|
+
}
|
|
52
|
+
function isReconnectable(account, reconnectableExternalIds) {
|
|
53
|
+
return isPresent(account.externalId) && reconnectableExternalIds.has(account.externalId);
|
|
54
|
+
}
|
|
55
|
+
function isAlreadyConnected(account, alreadyConnectedExternalIds, reconnectableExternalIds) {
|
|
56
|
+
return (isPresent(account.externalId) &&
|
|
57
|
+
alreadyConnectedExternalIds.has(account.externalId) &&
|
|
58
|
+
!isReconnectable(account, reconnectableExternalIds));
|
|
59
|
+
}
|
|
60
|
+
export function isSelectableAccount(account, alreadyConnectedExternalIds, reconnectableExternalIds = new Set()) {
|
|
61
|
+
return (account.isConnectable === true &&
|
|
62
|
+
!isAlreadyConnected(account, alreadyConnectedExternalIds, reconnectableExternalIds));
|
|
63
|
+
}
|
|
64
|
+
export function accountHint(account, alreadyConnectedExternalIds, reconnectableExternalIds = new Set()) {
|
|
65
|
+
if (account.isConnectable !== true)
|
|
66
|
+
return unavailableReasonLabel(account.unavailableReason);
|
|
67
|
+
if (isReconnectable(account, reconnectableExternalIds))
|
|
68
|
+
return "Reconnect";
|
|
69
|
+
if (isAlreadyConnected(account, alreadyConnectedExternalIds, reconnectableExternalIds))
|
|
70
|
+
return "Already connected";
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
export function MetaAdsConnectForm(props) {
|
|
74
|
+
const { onBack, onChanged, onDismiss, provider, resumedFromOAuth = false, importNotice } = props;
|
|
75
|
+
const { mantaClient } = useMantaClient();
|
|
76
|
+
const queryClient = useQueryClient();
|
|
77
|
+
const [hasStartedOAuth, setHasStartedOAuth] = useState(resumedFromOAuth);
|
|
78
|
+
const [hasSeenActiveState, setHasSeenActiveState] = useState(resumedFromOAuth);
|
|
79
|
+
const [selectedAccountId, setSelectedAccountId] = useState("");
|
|
80
|
+
const hasDismissedRef = useRef(false);
|
|
81
|
+
const progressQuery = useMetaSetupProgressQuery(mantaClient, undefined, { staleTime: 0 });
|
|
82
|
+
const progress = progressQuery.data?.metaSetupProgress;
|
|
83
|
+
const phase = derivePhase({ states: progress?.state, hasStartedOAuth, hasSeenActiveState });
|
|
84
|
+
useWindowFocusPoll(progressQuery.refetch, { enabled: activePhases.includes(phase), intervalMs: pollIntervalMs });
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const states = progress?.state;
|
|
87
|
+
if (!isPresent(states))
|
|
88
|
+
return;
|
|
89
|
+
const isActive = states.some((state) => !terminalStates.includes(state) && state !== GQLMetaSetupStateEnum.NotFound);
|
|
90
|
+
if (isActive)
|
|
91
|
+
setHasSeenActiveState(true);
|
|
92
|
+
}, [progress]);
|
|
93
|
+
const connectedAccountsQuery = useConnectedAccountsQuery(mantaClient, undefined);
|
|
94
|
+
const alreadyConnectedExternalIds = new Set((connectedAccountsQuery.data?.connectedAccounts ?? [])
|
|
95
|
+
.filter((account) => account.integration === GQLIntegration.Meta)
|
|
96
|
+
.map((account) => account.externalId));
|
|
97
|
+
const reconnectableExternalIds = useReconnectableExternalIds(GQLIntegration.Meta);
|
|
98
|
+
const connectMutation = useMetaSetupConnectMutation(mantaClient);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (phase !== "done" || hasDismissedRef.current)
|
|
101
|
+
return;
|
|
102
|
+
hasDismissedRef.current = true;
|
|
103
|
+
toast.success("Meta connected");
|
|
104
|
+
onChanged();
|
|
105
|
+
if (!importNotice)
|
|
106
|
+
onDismiss();
|
|
107
|
+
}, [phase, onChanged, onDismiss, importNotice]);
|
|
108
|
+
function resetProgressCache() {
|
|
109
|
+
queryClient.setQueryData(useMetaSetupProgressQuery.getKey(), {
|
|
110
|
+
metaSetupProgress: { state: [], accounts: null, lastError: null },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function handleOpened() {
|
|
114
|
+
resetProgressCache();
|
|
115
|
+
setHasSeenActiveState(false);
|
|
116
|
+
setHasStartedOAuth(true);
|
|
117
|
+
}
|
|
118
|
+
async function handleSubmit() {
|
|
119
|
+
if (!selectedAccountId)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
await connectMutation.mutateAsync({ accountIds: [selectedAccountId] });
|
|
123
|
+
await progressQuery.refetch();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const info = parseError(err, "Failed to connect Meta");
|
|
127
|
+
toast.error(info.message, { description: info.description });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function handleRetry() {
|
|
131
|
+
setSelectedAccountId("");
|
|
132
|
+
hasDismissedRef.current = false;
|
|
133
|
+
resetProgressCache();
|
|
134
|
+
setHasStartedOAuth(false);
|
|
135
|
+
setHasSeenActiveState(false);
|
|
136
|
+
}
|
|
137
|
+
return (_jsxs("div", { className: "grid gap-6", children: [_jsx(ConnectFormHeader, { provider: provider, status: phase === "done" ? "Connected" : "Not connected" }), _jsx("div", { className: "min-h-40", children: match(phase)
|
|
138
|
+
.with("idle", () => _jsx(IdleView, {}))
|
|
139
|
+
.with("awaitingOAuth", () => _jsx(AwaitingOAuthView, {}))
|
|
140
|
+
.with("loadingAccounts", () => _jsx(LoadingAccountsView, {}))
|
|
141
|
+
.with("pickingAccount", () => (_jsx(PickingAccountView, { accounts: progress?.accounts ?? [], alreadyConnectedExternalIds: alreadyConnectedExternalIds, reconnectableExternalIds: reconnectableExternalIds, selectedAccountId: selectedAccountId, onSelect: setSelectedAccountId })))
|
|
142
|
+
.with("submitting", () => _jsx(SubmittingView, {}))
|
|
143
|
+
.with("done", () => _jsx(DoneView, { providerName: provider.name, importNotice: importNotice }))
|
|
144
|
+
.with("failed", () => (_jsx(FailedView, { title: progress?.lastError?.title, message: progress?.lastError?.message, onRetry: handleRetry })))
|
|
145
|
+
.exhaustive() }), _jsxs("div", { className: cn("flex items-center pt-2", phase === "done" ? "justify-end" : "justify-between"), children: [phase !== "done" && (_jsx(Button, { variant: "tertiary", onClick: onBack, icon: ArrowLeft, children: "Back" })), phase === "idle" && (_jsx(OAuthConnectLink, { href: "/oauth/meta/start", label: "Continue with Meta", onOpened: handleOpened })), phase === "pickingAccount" && (_jsx(Button, { onClick: () => void handleSubmit(), disabled: !selectedAccountId, isLoading: connectMutation.isPending, children: "Connect account" })), phase === "done" && importNotice && _jsx(Button, { onClick: onDismiss, children: "Done" })] })] }));
|
|
146
|
+
}
|
|
147
|
+
function IdleView() {
|
|
148
|
+
return (_jsxs("div", { className: "grid gap-1", children: [_jsx("h2", { className: "typography-large-body-semibold text-default-foreground", children: "Sign in with Meta" }), _jsx("p", { className: "typography-caption text-muted-foreground", children: "We'll take you to Meta sign-in so you can grant Manta access to your Meta ad accounts." })] }));
|
|
149
|
+
}
|
|
150
|
+
function AwaitingOAuthView() {
|
|
151
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("h2", { className: "typography-large-body-semibold text-default-foreground", children: "Waiting for Meta sign-in" }), _jsx("p", { className: "typography-caption text-muted-foreground", children: "Finish signing in to Meta. We'll pick it up automatically when you come back." })] }), _jsxs("div", { className: "flex items-center gap-2 text-muted-foreground typography-caption", role: "status", "aria-live": "polite", children: [_jsx(Loader, { className: "size-4", "aria-hidden": true }), " Listening for the connection\u2026"] })] }));
|
|
152
|
+
}
|
|
153
|
+
function LoadingAccountsView() {
|
|
154
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("h2", { className: "typography-large-body-semibold text-default-foreground", children: "Loading your Meta ad accounts" }), _jsx("p", { className: "typography-caption text-muted-foreground", children: "This usually takes a few seconds." })] }), _jsxs("div", { className: "flex items-center gap-2 text-muted-foreground typography-caption", role: "status", "aria-live": "polite", children: [_jsx(Loader, { className: "size-4", "aria-hidden": true }), " Fetching accounts\u2026"] })] }));
|
|
155
|
+
}
|
|
156
|
+
function PickingAccountView(props) {
|
|
157
|
+
const { accounts, alreadyConnectedExternalIds, reconnectableExternalIds, selectedAccountId, onSelect } = props;
|
|
158
|
+
if (!isPresent(accounts)) {
|
|
159
|
+
return (_jsx(Banner, { type: "warning", label: "We couldn't find any Meta ad accounts on this Facebook identity. Sign in with a different account or check your Meta ad account access." }));
|
|
160
|
+
}
|
|
161
|
+
const selectableCount = accounts.filter((account) => isSelectableAccount(account, alreadyConnectedExternalIds, reconnectableExternalIds)).length;
|
|
162
|
+
if (selectableCount === 0) {
|
|
163
|
+
return (_jsx(Banner, { type: "info", label: "No connectable Meta ad accounts remain on this Facebook identity \u2014 they are already connected or unavailable. Sign in with a different Facebook account to connect a new one." }));
|
|
164
|
+
}
|
|
165
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("h2", { className: "typography-large-body-semibold text-default-foreground", children: "Pick the Meta ad account to connect" }), _jsx("p", { className: "typography-caption text-muted-foreground", children: "Choose the account whose campaigns Manta should manage." })] }), _jsxs(Select, { value: selectedAccountId, onValueChange: onSelect, children: [_jsx(SelectTrigger, { label: "Meta ad account", id: "meta-ad-account", className: "w-full h-10", children: _jsx(SelectValue, { placeholder: "Select an account" }) }), _jsx(SelectContent, { children: accounts.map((account) => {
|
|
166
|
+
const label = account.name ?? account.id;
|
|
167
|
+
const hint = accountHint(account, alreadyConnectedExternalIds, reconnectableExternalIds);
|
|
168
|
+
return (_jsx(SelectItem, { value: account.id, disabled: !isSelectableAccount(account, alreadyConnectedExternalIds, reconnectableExternalIds), children: hint ? `${label} · ${hint}` : label }, account.id));
|
|
169
|
+
}) })] })] }));
|
|
170
|
+
}
|
|
171
|
+
function SubmittingView() {
|
|
172
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: "grid gap-1", children: [_jsx("h2", { className: "typography-large-body-semibold text-default-foreground", children: "Connecting your account" }), _jsx("p", { className: "typography-caption text-muted-foreground", children: "Hold tight \u2014 this usually takes a few seconds." })] }), _jsxs("div", { className: "flex items-center gap-2 text-muted-foreground typography-caption", role: "status", "aria-live": "polite", children: [_jsx(Loader, { className: "size-4", "aria-hidden": true }), " Provisioning\u2026"] })] }));
|
|
173
|
+
}
|
|
174
|
+
function DoneView(props) {
|
|
175
|
+
const { providerName, importNotice } = props;
|
|
176
|
+
if (importNotice)
|
|
177
|
+
return _jsx(ImportStartedNotice, { providerName: providerName, notice: importNotice });
|
|
178
|
+
return (_jsxs("div", { className: "grid gap-1", children: [_jsx("h2", { className: "typography-large-body-semibold text-default-foreground", children: "Meta connected" }), _jsx("p", { className: "typography-caption text-muted-foreground", children: "Closing this dialog\u2026" })] }));
|
|
179
|
+
}
|
|
180
|
+
function FailedView(props) {
|
|
181
|
+
const { title, message, onRetry } = props;
|
|
182
|
+
return (_jsxs("div", { className: "grid gap-4", children: [_jsx(Banner, { type: "error", size: "loose", label: title ?? "Meta connection failed", description: message ?? undefined }), _jsx(Button, { onClick: onRetry, children: "Try again" })] }));
|
|
183
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ButtonVariant } from "#components/button/index";
|
|
2
|
+
type OAuthConnectLinkProps = {
|
|
3
|
+
href: string;
|
|
4
|
+
label: string;
|
|
5
|
+
onOpened: () => void;
|
|
6
|
+
variant?: ButtonVariant;
|
|
7
|
+
};
|
|
8
|
+
export declare function OAuthConnectLink(props: OAuthConnectLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=OAuthConnectLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OAuthConnectLink.d.ts","sourceRoot":"","sources":["../../../../../src/routes/integrations/components/connect-forms/OAuthConnectLink.tsx"],"names":[],"mappings":"AACA,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGpE,KAAK,qBAAqB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB,CAAC;AAOF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,2CAkB5D"}
|