@manta-eu/react 0.3.0 → 0.4.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.
Files changed (111) hide show
  1. package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
  2. package/dist/components/planning/plan-detail-utils.js +2 -0
  3. package/dist/components/planning/plan-detail-utils.js.map +1 -1
  4. package/dist/graph/EntityCard.d.ts.map +1 -1
  5. package/dist/graph/EntityCard.js +2 -1
  6. package/dist/graph/EntityCard.js.map +1 -1
  7. package/dist/graph/EntityViewWithLinks.d.ts +7 -0
  8. package/dist/graph/EntityViewWithLinks.d.ts.map +1 -0
  9. package/dist/graph/EntityViewWithLinks.js +28 -0
  10. package/dist/graph/EntityViewWithLinks.js.map +1 -0
  11. package/dist/graph/card-links.d.ts +19 -0
  12. package/dist/graph/card-links.d.ts.map +1 -0
  13. package/dist/graph/card-links.js +23 -0
  14. package/dist/graph/card-links.js.map +1 -0
  15. package/dist/graph/nodes/BrandAssetAnalysisView.d.ts.map +1 -1
  16. package/dist/graph/nodes/BrandAssetAnalysisView.js +1 -1
  17. package/dist/graph/nodes/BrandAssetAnalysisView.js.map +1 -1
  18. package/dist/graph/nodes/BrandFontSuggestionView.d.ts.map +1 -1
  19. package/dist/graph/nodes/BrandFontSuggestionView.js +1 -1
  20. package/dist/graph/nodes/BrandFontSuggestionView.js.map +1 -1
  21. package/dist/graph/nodes/EntityCardShell.d.ts +2 -0
  22. package/dist/graph/nodes/EntityCardShell.d.ts.map +1 -1
  23. package/dist/graph/nodes/EntityCardShell.js +13 -3
  24. package/dist/graph/nodes/EntityCardShell.js.map +1 -1
  25. package/dist/graph/nodes/NodePropertyTable.d.ts +2 -1
  26. package/dist/graph/nodes/NodePropertyTable.d.ts.map +1 -1
  27. package/dist/graph/nodes/NodePropertyTable.js +10 -1
  28. package/dist/graph/nodes/NodePropertyTable.js.map +1 -1
  29. package/dist/graph/nodes/node-property-display.d.ts +0 -1
  30. package/dist/graph/nodes/node-property-display.d.ts.map +1 -1
  31. package/dist/graph/nodes/node-property-display.js +2 -6
  32. package/dist/graph/nodes/node-property-display.js.map +1 -1
  33. package/dist/graph/special-nodes/AttributeList.d.ts.map +1 -1
  34. package/dist/graph/special-nodes/AttributeList.js +3 -2
  35. package/dist/graph/special-nodes/AttributeList.js.map +1 -1
  36. package/dist/graph/special-nodes/RelatedSection.d.ts +6 -0
  37. package/dist/graph/special-nodes/RelatedSection.d.ts.map +1 -0
  38. package/dist/graph/special-nodes/RelatedSection.js +16 -0
  39. package/dist/graph/special-nodes/RelatedSection.js.map +1 -0
  40. package/dist/graph/special-nodes/card-facts.d.ts +2 -0
  41. package/dist/graph/special-nodes/card-facts.d.ts.map +1 -1
  42. package/dist/graph/special-nodes/card-facts.js +11 -0
  43. package/dist/graph/special-nodes/card-facts.js.map +1 -1
  44. package/dist/graph/types.d.ts +2 -2
  45. package/dist/graph/types.d.ts.map +1 -1
  46. package/dist/hooks/use-approve-anyway.d.ts.map +1 -1
  47. package/dist/hooks/use-approve-anyway.js +8 -8
  48. package/dist/hooks/use-approve-anyway.js.map +1 -1
  49. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  50. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  51. package/dist/ldo-ui.d.ts +1 -0
  52. package/dist/ldo-ui.d.ts.map +1 -1
  53. package/dist/ldo-ui.js +1 -0
  54. package/dist/ldo-ui.js.map +1 -1
  55. package/dist/routes/data-explorer/components/record-detail/ActionFormDialog.d.ts.map +1 -1
  56. package/dist/routes/data-explorer/components/record-detail/ActionFormDialog.js +7 -21
  57. package/dist/routes/data-explorer/components/record-detail/ActionFormDialog.js.map +1 -1
  58. package/dist/routes/data-explorer/components/record-detail/ChatPreview.d.ts.map +1 -1
  59. package/dist/routes/data-explorer/components/record-detail/ChatPreview.js +2 -2
  60. package/dist/routes/data-explorer/components/record-detail/ChatPreview.js.map +1 -1
  61. package/dist/routes/data-explorer/components/record-detail/action-properties.d.ts +2 -2
  62. package/dist/routes/data-explorer/components/record-detail/action-properties.d.ts.map +1 -1
  63. package/dist/routes/data-explorer/components/record-detail/action-properties.js +3 -3
  64. package/dist/routes/data-explorer/components/record-detail/action-properties.js.map +1 -1
  65. package/dist/routes/integrations/catalog-shape.d.ts +1 -0
  66. package/dist/routes/integrations/catalog-shape.d.ts.map +1 -1
  67. package/dist/routes/integrations/catalog-shape.js.map +1 -1
  68. package/dist/routes/integrations/components/connect-forms/MicrosoftAdsConnectForm.d.ts +7 -0
  69. package/dist/routes/integrations/components/connect-forms/MicrosoftAdsConnectForm.d.ts.map +1 -0
  70. package/dist/routes/integrations/components/connect-forms/MicrosoftAdsConnectForm.js +76 -0
  71. package/dist/routes/integrations/components/connect-forms/MicrosoftAdsConnectForm.js.map +1 -0
  72. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.d.ts +7 -0
  73. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.d.ts.map +1 -1
  74. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js +8 -7
  75. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js.map +1 -1
  76. package/dist/routes/integrations/components/connect-forms/inferal-locations.d.ts +3 -0
  77. package/dist/routes/integrations/components/connect-forms/inferal-locations.d.ts.map +1 -0
  78. package/dist/routes/integrations/components/connect-forms/inferal-locations.js +13 -0
  79. package/dist/routes/integrations/components/connect-forms/inferal-locations.js.map +1 -0
  80. package/dist/routes/integrations/connect-forms.d.ts +2 -0
  81. package/dist/routes/integrations/connect-forms.d.ts.map +1 -1
  82. package/dist/routes/integrations/connect-forms.js +2 -0
  83. package/dist/routes/integrations/connect-forms.js.map +1 -1
  84. package/dist/routes/integrations/connection-view.d.ts.map +1 -1
  85. package/dist/routes/integrations/connection-view.js +1 -0
  86. package/dist/routes/integrations/connection-view.js.map +1 -1
  87. package/package.json +7 -7
  88. package/src/components/planning/plan-detail-utils.tsx +2 -0
  89. package/src/graph/EntityCard.tsx +2 -1
  90. package/src/graph/EntityViewWithLinks.tsx +43 -0
  91. package/src/graph/card-links.tsx +43 -0
  92. package/src/graph/nodes/BrandAssetAnalysisView.tsx +1 -0
  93. package/src/graph/nodes/BrandFontSuggestionView.tsx +1 -0
  94. package/src/graph/nodes/EntityCardShell.tsx +37 -3
  95. package/src/graph/nodes/NodePropertyTable.tsx +16 -2
  96. package/src/graph/nodes/node-property-display.ts +2 -7
  97. package/src/graph/special-nodes/AttributeList.tsx +4 -2
  98. package/src/graph/special-nodes/RelatedSection.tsx +29 -0
  99. package/src/graph/special-nodes/card-facts.ts +13 -0
  100. package/src/graph/types.ts +1 -1
  101. package/src/hooks/use-approve-anyway.ts +8 -11
  102. package/src/ldo-ui.ts +1 -0
  103. package/src/routes/data-explorer/components/record-detail/ActionFormDialog.tsx +7 -29
  104. package/src/routes/data-explorer/components/record-detail/ChatPreview.tsx +6 -1
  105. package/src/routes/data-explorer/components/record-detail/action-properties.ts +3 -3
  106. package/src/routes/integrations/catalog-shape.ts +2 -0
  107. package/src/routes/integrations/components/connect-forms/MicrosoftAdsConnectForm.tsx +140 -0
  108. package/src/routes/integrations/components/connect-forms/OAuthSetupConnectForm.tsx +27 -5
  109. package/src/routes/integrations/components/connect-forms/inferal-locations.ts +13 -0
  110. package/src/routes/integrations/connect-forms.ts +2 -0
  111. package/src/routes/integrations/connection-view.ts +1 -0
@@ -1,4 +1,6 @@
1
1
  import { useExecuteAgentActionAdditionMutation } from "@manta-eu/client";
2
+ import { entityActionInput } from "@manta-eu/client/shared/action-document";
3
+ import { BrandAssetDecisionSchema } from "@manta-eu/ldo/schemas/brand";
2
4
  import { isPresent } from "@manta-eu/lib/present";
3
5
  import { isValidFullIri, namespaces } from "@manta-eu/rdf-lib/namespaces";
4
6
  import { useQueryClient } from "@tanstack/react-query";
@@ -8,13 +10,12 @@ import { useCallback, useMemo, useRef, useState } from "react";
8
10
  import { toast } from "sonner";
9
11
  import type { ManualAction } from "./use-manual-actions";
10
12
  import { useSparqlSelect } from "./use-sparql-construct";
11
- import { buildActionDocument, mintSubjectIri } from "#lib/action-document";
13
+ import { mintSubjectIri } from "#lib/action-document";
12
14
  import { useMantaClient } from "#providers/MantaClientProvider";
13
15
  import { invalidateActionPlanQueries } from "#shared/query-keys";
14
16
 
15
17
  const df = new DataFactory();
16
18
  const assetAnalysis = `${namespaces.brand}AssetAnalysis`;
17
- const assetDecision = `${namespaces.brand}AssetDecision`;
18
19
  const decideAssetShape = `${namespaces.brand}DecideAssetShape`;
19
20
 
20
21
  export type ApproveAnywayHandle = {
@@ -68,16 +69,12 @@ export function useApproveAnyway(planGraphIri: string): ApproveAnywayHandle {
68
69
  if (inFlight.current.has(analysisIri)) return;
69
70
  inFlight.current.add(analysisIri);
70
71
  setApproving(new Set(inFlight.current));
71
- const subjectIri = mintSubjectIri(assetDecision);
72
- const properties = buildActionDocument(
73
- { subjectIri, rdfType: assetDecision },
74
- {
75
- [`${namespaces.brand}decisionOnAsset`]: { "@id": analysisIri },
76
- [`${namespaces.brand}decision`]: "approved",
77
- },
78
- );
79
72
  void startAddition({
80
- input: { subjectIri, rdfType: assetDecision, actionShape: decideAssetShape, properties },
73
+ input: entityActionInput(BrandAssetDecisionSchema, decideAssetShape, {
74
+ $id: mintSubjectIri(BrandAssetDecisionSchema["@type"]),
75
+ decisionOnAsset: analysisIri,
76
+ decision: "approved",
77
+ }),
81
78
  })
82
79
  .then(({ executeActionAddition }) => {
83
80
  if (!executeActionAddition.success) throw new Error(executeActionAddition.error ?? "The approval failed");
package/src/ldo-ui.ts CHANGED
@@ -344,6 +344,7 @@ export {
344
344
  export { NodeFrame, type NodeFrameProps } from "./graph/special-nodes/NodeFrame";
345
345
 
346
346
  export { EntityCard, findEntityNode, type EntityCardProps } from "./graph/EntityCard";
347
+ export { EntityViewWithLinks, type EntityViewWithLinksProps } from "./graph/EntityViewWithLinks";
347
348
  export { findDetailViewForTypes, findPluralViewForTypes, type DetailView, type PluralView } from "./graph/detail-views";
348
349
  export { EntityStripDisplay, type EntityStripDisplayProps } from "./graph/EntityStripDisplay";
349
350
  export { stubNodeData } from "./graph/stub-node";
@@ -59,7 +59,6 @@ export function ActionFormDialog(props: ActionFormDialogProps) {
59
59
  const baselineRef = useRef<PropertyMap | null>(null);
60
60
  const [refusal, setRefusal] = useState<string | null>(null);
61
61
  const [formElement, setFormElement] = useState<ShaclFormElement | null>(null);
62
- const [isReadingForm, setIsReadingForm] = useState(false);
63
62
 
64
63
  const attachForm = useCallback((element: ShaclFormElement | null) => setFormElement(element), []);
65
64
 
@@ -77,29 +76,20 @@ export function ActionFormDialog(props: ActionFormDialogProps) {
77
76
  useEffect(() => {
78
77
  if (!formElement || !isUpdate) return;
79
78
 
80
- let cancelled = false;
81
-
82
79
  // Baseline is the form's own serialization: its editors normalize literals, the triples do not.
83
- async function captureBaseline() {
80
+ function onReady() {
84
81
  const store = formElement?.toRDF();
85
- const baseline = store ? await baselineFromStore(store, subjectIri) : null;
86
-
87
- if (baseline && !cancelled) baselineRef.current = baseline;
88
- }
82
+ const baseline = store ? baselineFromStore(store, subjectIri) : null;
89
83
 
90
- function onReady() {
91
- void captureBaseline();
84
+ if (baseline) baselineRef.current = baseline;
92
85
  }
93
86
 
94
87
  onReady();
95
88
  formElement.addEventListener("ready", onReady);
96
- return () => {
97
- cancelled = true;
98
- formElement.removeEventListener("ready", onReady);
99
- };
89
+ return () => formElement.removeEventListener("ready", onReady);
100
90
  }, [formElement, isUpdate, subjectIri]);
101
91
 
102
- async function submit() {
92
+ function submit() {
103
93
  const store = formElement?.toRDF();
104
94
  const baseline = baselineRef.current;
105
95
 
@@ -108,14 +98,7 @@ export function ActionFormDialog(props: ActionFormDialogProps) {
108
98
  return;
109
99
  }
110
100
 
111
- setIsReadingForm(true);
112
- const edited = await propertiesFromStore(store, subjectIri).catch(() => null);
113
- setIsReadingForm(false);
114
-
115
- if (!edited) {
116
- setRefusal("The form could not be read.");
117
- return;
118
- }
101
+ const edited = propertiesFromStore(store, subjectIri);
119
102
 
120
103
  if (isUpdate && baseline) {
121
104
  const cleared = removedPredicates(baseline, edited);
@@ -176,12 +159,7 @@ export function ActionFormDialog(props: ActionFormDialogProps) {
176
159
  <Button variant="tertiary" size="sm" onClick={() => onOpenChange(false)}>
177
160
  Cancel
178
161
  </Button>
179
- <Button
180
- size="sm"
181
- onClick={() => void submit()}
182
- isLoading={isPending || isReadingForm}
183
- disabled={!shapesTurtle}
184
- >
162
+ <Button size="sm" onClick={submit} isLoading={isPending} disabled={!shapesTurtle}>
185
163
  {copy.submitLabel}
186
164
  </Button>
187
165
  </DialogFooter>
@@ -7,6 +7,7 @@ import { actionShapesForTypes } from "./action-shapes";
7
7
  import { Badge } from "#components/badge/index";
8
8
  import {
9
9
  type ActionShapeSpec,
10
+ EntityViewWithLinks,
10
11
  findEntityNode,
11
12
  GenericView,
12
13
  getEntityView,
@@ -97,7 +98,11 @@ export function ChatPreview(props: ChatPreviewProps) {
97
98
  <section>
98
99
  <h3 className={cn(sectionLabelClass, "mb-1.5 text-muted-foreground")}>Chat rendering</h3>
99
100
  <div className="max-w-md">
100
- {EntityView ? <EntityView data={node.data} /> : <GenericView data={node.data} defaultExpanded />}
101
+ {EntityView ? (
102
+ <EntityViewWithLinks view={EntityView} data={node.data} />
103
+ ) : (
104
+ <GenericView data={node.data} defaultExpanded />
105
+ )}
101
106
  </div>
102
107
  </section>
103
108
 
@@ -7,14 +7,14 @@ export type PropertyMap = Record<string, unknown>;
7
7
  // The form stamps this on every serialization
8
8
  const conformsTo = `${namespaces.dcterms}conformsTo`;
9
9
 
10
- export async function propertiesFromStore(store: ShaclFormStore, subjectIri: string): Promise<PropertyMap> {
10
+ export function propertiesFromStore(store: ShaclFormStore, subjectIri: string): PropertyMap {
11
11
  const quads = [...store].filter((quad) => quad.predicate.value !== conformsTo);
12
- const { "@id": _id, "@type": _type, ...properties } = await documentFromQuads(quads, subjectIri);
12
+ const { "@id": _id, "@type": _type, ...properties } = documentFromQuads(quads, subjectIri);
13
13
 
14
14
  return properties;
15
15
  }
16
16
 
17
- export async function baselineFromStore(store: ShaclFormStore, subjectIri: string): Promise<PropertyMap | null> {
17
+ export function baselineFromStore(store: ShaclFormStore, subjectIri: string): PropertyMap | null {
18
18
  if (store.size === 0) return null;
19
19
 
20
20
  return propertiesFromStore(store, subjectIri);
@@ -22,6 +22,8 @@ export type OAuthConnectionMethod = ConnectionMethodBase & {
22
22
  source: "oauth";
23
23
  integration: GQLIntegration;
24
24
  callbackProvider?: string;
25
+ // Further oauth_provider values whose callback resumes this same method, e.g. a second identity provider.
26
+ alternateCallbackProviders?: readonly string[];
25
27
  resumesSetupAfterOAuth?: boolean;
26
28
  connectedAction?: ComponentType;
27
29
  };
@@ -0,0 +1,140 @@
1
+ import {
2
+ GQLInferalLocation,
3
+ GQLIntegration,
4
+ useMicrosoftAdsSetupConnectMutation,
5
+ useMicrosoftAdsSetupProgressQuery,
6
+ type GQLMicrosoftAdsAccount,
7
+ } from "@manta-eu/client";
8
+ import { useQueryClient } from "@tanstack/react-query";
9
+ import { useState } from "react";
10
+ import { inferalLocationLabels } from "./inferal-locations";
11
+ import { OAuthSetupConnectForm, type OAuthSetup, type SetupCopy } from "./OAuthSetupConnectForm";
12
+ import type { ConnectFormProps } from "./types";
13
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "#components/select/index";
14
+ import type { TreeSelectItem } from "#components/tree-multi-select/index";
15
+ import { useMantaClient } from "#providers/index";
16
+
17
+ const copy: SetupCopy = {
18
+ providerName: "Microsoft Ads",
19
+ identityProviderName: "Microsoft",
20
+ entitySingular: "account",
21
+ entityPlural: "accounts",
22
+ signInDescription:
23
+ "Sign in the way you sign in to Microsoft Advertising: with a Microsoft account, or with Google if your Advertising login is a Google account.",
24
+ pickTitle: "Pick the Microsoft Ads accounts to connect",
25
+ pickDescription:
26
+ "Manta loads each account's Search campaigns, ad groups, ads and keywords, plus daily campaign, keyword and search-query reports.",
27
+ noEntitiesLabel:
28
+ "We couldn't find any Microsoft Advertising accounts for this Microsoft sign-in. Sign in with a different user or check your account access.",
29
+ noneConnectableLabel:
30
+ "Every Microsoft Advertising account on this sign-in is already connected. Sign in as a different user to connect another one.",
31
+ };
32
+
33
+ export const defaultMicrosoftAdsLocation = GQLInferalLocation.Enam;
34
+
35
+ // Microsoft Advertising refuses a Microsoft token for a login that is a Google account (GoogleAccountIsRequired).
36
+ const googleSignIn = [{ label: "Continue with Google", href: "/oauth/microsoft-ads-google/start" }] as const;
37
+
38
+ const inferalLocations: ReadonlySet<string> = new Set(Object.values(GQLInferalLocation));
39
+
40
+ function isInferalLocation(value: string): value is GQLInferalLocation {
41
+ return inferalLocations.has(value);
42
+ }
43
+
44
+ function accountHint(
45
+ account: GQLMicrosoftAdsAccount,
46
+ alreadyConnected: boolean,
47
+ reconnectable: boolean,
48
+ ): string | undefined {
49
+ if (!account.connectable) return "Unavailable";
50
+ if (reconnectable) return "Reconnect";
51
+ if (alreadyConnected) return "Already connected";
52
+ return undefined;
53
+ }
54
+
55
+ export function microsoftAdsTreeItems(
56
+ accounts: GQLMicrosoftAdsAccount[],
57
+ alreadyConnectedExternalIds: ReadonlySet<string>,
58
+ reconnectableExternalIds: ReadonlySet<string> = new Set(),
59
+ ): TreeSelectItem[] {
60
+ return accounts.map((account) => {
61
+ const reconnectable = reconnectableExternalIds.has(account.id);
62
+ const alreadyConnected = !reconnectable && alreadyConnectedExternalIds.has(account.id);
63
+ return {
64
+ id: account.id,
65
+ label: account.name ?? account.number ?? account.id,
66
+ caption: account.number ?? account.id,
67
+ hint: accountHint(account, alreadyConnected, reconnectable),
68
+ selectable: account.connectable,
69
+ disabled: alreadyConnected,
70
+ depth: 0,
71
+ };
72
+ });
73
+ }
74
+
75
+ function useMicrosoftAdsSetup(location: GQLInferalLocation): OAuthSetup<GQLMicrosoftAdsAccount> {
76
+ const { mantaClient } = useMantaClient();
77
+ const queryClient = useQueryClient();
78
+ const progressQuery = useMicrosoftAdsSetupProgressQuery(mantaClient, undefined, { staleTime: 0 });
79
+ const connectMutation = useMicrosoftAdsSetupConnectMutation(mantaClient);
80
+ const progress = progressQuery.data?.microsoftAdsSetupProgress;
81
+
82
+ return {
83
+ progress: progress && { state: progress.state, rows: progress.accounts, lastError: progress.lastError },
84
+ refetchProgress: progressQuery.refetch,
85
+ resetProgress: () =>
86
+ queryClient.setQueryData(useMicrosoftAdsSetupProgressQuery.getKey(), {
87
+ microsoftAdsSetupProgress: { state: [], accounts: null, lastError: null },
88
+ }),
89
+ connect: async (accountIds) =>
90
+ (await connectMutation.mutateAsync({ accountIds, location })).microsoftAdsSetupConnect,
91
+ isConnecting: connectMutation.isPending,
92
+ };
93
+ }
94
+
95
+ type LocationPickerProps = {
96
+ value: GQLInferalLocation;
97
+ onChange: (location: GQLInferalLocation) => void;
98
+ };
99
+
100
+ function LocationPicker(props: LocationPickerProps) {
101
+ const { value, onChange } = props;
102
+
103
+ return (
104
+ <div className="grid gap-1">
105
+ <Select value={value} onValueChange={(next) => isInferalLocation(next) && onChange(next)}>
106
+ <SelectTrigger label="Stream location" id="microsoft-ads-location" className="w-full h-10">
107
+ <SelectValue placeholder="Select a region" />
108
+ </SelectTrigger>
109
+ <SelectContent>
110
+ {Object.entries(inferalLocationLabels).map(([location, label]) => (
111
+ <SelectItem key={location} value={location}>
112
+ {label}
113
+ </SelectItem>
114
+ ))}
115
+ </SelectContent>
116
+ </Select>
117
+ <p className="typography-caption text-muted-foreground">
118
+ Where the account's data stream runs. Pick the region closest to your account.
119
+ </p>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ export function MicrosoftAdsConnectForm(props: ConnectFormProps) {
125
+ const [location, setLocation] = useState<GQLInferalLocation>(defaultMicrosoftAdsLocation);
126
+ const setup = useMicrosoftAdsSetup(location);
127
+
128
+ return (
129
+ <OAuthSetupConnectForm
130
+ {...props}
131
+ integration={GQLIntegration.MicrosoftAds}
132
+ oauthStartPath="/oauth/microsoft-ads/start"
133
+ copy={copy}
134
+ setup={setup}
135
+ treeItems={microsoftAdsTreeItems}
136
+ pickExtras={<LocationPicker value={location} onChange={setLocation} />}
137
+ alternateSignIns={googleSignIn}
138
+ />
139
+ );
140
+ }
@@ -2,7 +2,7 @@ import { GQLGoogleSetupStateEnum, type GQLIntegration } from "@manta-eu/client";
2
2
  import { parseError } from "@manta-eu/client/shared/error-utils";
3
3
  import { isPresent } from "@manta-eu/lib/present";
4
4
  import { ArrowLeft } from "lucide-react";
5
- import { useEffect, useRef, useState } from "react";
5
+ import { useEffect, useRef, useState, type ReactNode } from "react";
6
6
  import { toast } from "sonner";
7
7
  import { match } from "ts-pattern";
8
8
  import { ConnectFormHeader } from "./ConnectFormHeader";
@@ -66,11 +66,19 @@ export type OAuthSetupConnectFormProps<Row extends SetupRow> = ConnectFormProps
66
66
  ) => TreeSelectItem[];
67
67
  treeOptions?: SetupTreeOptions;
68
68
  resumesUnfinishedSetup?: boolean;
69
+ // Rendered under the list while picking, for a choice the connect needs besides the ids.
70
+ pickExtras?: ReactNode;
71
+ // Other identity providers that lead into the same setup, offered beside the main sign-in.
72
+ alternateSignIns?: readonly AlternateSignIn[];
69
73
  };
70
74
 
75
+ export type AlternateSignIn = { label: string; href: string };
76
+
71
77
  export function OAuthSetupConnectForm<Row extends SetupRow>(props: OAuthSetupConnectFormProps<Row>) {
72
78
  const { onBack, onChanged, onDismiss, provider, resumedFromOAuth = false, importNotice } = props;
73
- const { integration, oauthStartPath, copy, setup, treeItems, treeOptions, resumesUnfinishedSetup } = props;
79
+ const { integration, oauthStartPath, copy, setup, treeItems, treeOptions, resumesUnfinishedSetup, pickExtras } =
80
+ props;
81
+ const { alternateSignIns } = props;
74
82
  const { providerName } = copy;
75
83
  const [hasStartedOAuth, setHasStartedOAuth] = useState(resumedFromOAuth);
76
84
  const [hasSeenActiveState, setHasSeenActiveState] = useState(resumedFromOAuth);
@@ -151,7 +159,14 @@ export function OAuthSetupConnectForm<Row extends SetupRow>(props: OAuthSetupCon
151
159
 
152
160
  <div className="min-h-40">
153
161
  {match(phase)
154
- .with("idle", () => <IdleView copy={copy} oauthStartPath={oauthStartPath} onOpened={handleOpened} />)
162
+ .with("idle", () => (
163
+ <IdleView
164
+ copy={copy}
165
+ oauthStartPath={oauthStartPath}
166
+ alternateSignIns={alternateSignIns}
167
+ onOpened={handleOpened}
168
+ />
169
+ ))
155
170
  .with("awaitingOAuth", () => <AwaitingOAuthView copy={copy} />)
156
171
  .with("loadingAccounts", () => <LoadingEntitiesView copy={copy} />)
157
172
  .with("pickingAccount", () => (
@@ -161,6 +176,7 @@ export function OAuthSetupConnectForm<Row extends SetupRow>(props: OAuthSetupCon
161
176
  selectedIds={selectedIds}
162
177
  onToggle={toggleId}
163
178
  treeOptions={treeOptions}
179
+ extras={pickExtras}
164
180
  />
165
181
  ))
166
182
  .with("submitting", () => <SubmittingView copy={copy} />)
@@ -221,11 +237,12 @@ type CopyProps = {
221
237
 
222
238
  type IdleViewProps = CopyProps & {
223
239
  oauthStartPath: string;
240
+ alternateSignIns?: readonly AlternateSignIn[];
224
241
  onOpened: () => void;
225
242
  };
226
243
 
227
244
  function IdleView(props: IdleViewProps) {
228
- const { copy, oauthStartPath, onOpened } = props;
245
+ const { copy, oauthStartPath, alternateSignIns, onOpened } = props;
229
246
 
230
247
  return (
231
248
  <div className="grid gap-4">
@@ -240,6 +257,9 @@ function IdleView(props: IdleViewProps) {
240
257
  label={`Continue with ${copy.identityProviderName}`}
241
258
  onOpened={onOpened}
242
259
  />
260
+ {alternateSignIns?.map(({ label, href }) => (
261
+ <OAuthConnectLink key={href} href={href} label={label} variant="secondary" onOpened={onOpened} />
262
+ ))}
243
263
  </div>
244
264
  );
245
265
  }
@@ -283,10 +303,11 @@ type PickingEntityViewProps = CopyProps & {
283
303
  selectedIds: ReadonlySet<string>;
284
304
  onToggle: (id: string, checked: boolean) => void;
285
305
  treeOptions?: SetupTreeOptions;
306
+ extras?: ReactNode;
286
307
  };
287
308
 
288
309
  function PickingEntityView(props: PickingEntityViewProps) {
289
- const { copy, items, selectedIds, onToggle, treeOptions } = props;
310
+ const { copy, items, selectedIds, onToggle, treeOptions, extras } = props;
290
311
 
291
312
  if (!isPresent(items)) {
292
313
  return <Banner type="warning" label={copy.noEntitiesLabel} />;
@@ -310,6 +331,7 @@ function PickingEntityView(props: PickingEntityViewProps) {
310
331
  searchPlaceholder={`Search ${copy.entityPlural}...`}
311
332
  {...treeOptions}
312
333
  />
334
+ {extras}
313
335
  </div>
314
336
  );
315
337
  }
@@ -0,0 +1,13 @@
1
+ import { GQLInferalLocation } from "@manta-eu/client";
2
+
3
+ export const inferalLocationLabels: Readonly<Record<GQLInferalLocation, string>> = {
4
+ [GQLInferalLocation.Wnam]: "Western North America",
5
+ [GQLInferalLocation.Enam]: "Eastern North America",
6
+ [GQLInferalLocation.Sam]: "South America",
7
+ [GQLInferalLocation.Weur]: "Western Europe",
8
+ [GQLInferalLocation.Eeur]: "Eastern Europe",
9
+ [GQLInferalLocation.Apac]: "Asia Pacific",
10
+ [GQLInferalLocation.Oc]: "Oceania",
11
+ [GQLInferalLocation.Afr]: "Africa",
12
+ [GQLInferalLocation.Me]: "Middle East",
13
+ };
@@ -14,6 +14,7 @@ export { GoogleAnalyticsConnectForm } from "./components/connect-forms/GoogleAna
14
14
  export { KlaviyoConnectForm } from "./components/connect-forms/KlaviyoConnectForm";
15
15
  export { LinkedinConnectForm } from "./components/connect-forms/LinkedinConnectForm";
16
16
  export { MetaAdsConnectForm } from "./components/connect-forms/MetaAdsConnectForm";
17
+ export { MicrosoftAdsConnectForm } from "./components/connect-forms/MicrosoftAdsConnectForm";
17
18
  export { ShopifyConnectForm } from "./components/connect-forms/ShopifyConnectForm";
18
19
  export { SlackConnectForm } from "./components/connect-forms/SlackConnectForm";
19
20
  export { TriplewhaleConnectForm } from "./components/connect-forms/TriplewhaleConnectForm";
@@ -25,3 +26,4 @@ export {
25
26
  type MantaSyncPluginLinkProps,
26
27
  type MantaSyncPluginStepProps,
27
28
  } from "./components/MantaSyncPluginDownload";
29
+ export { inferalLocationLabels } from "./components/connect-forms/inferal-locations";
@@ -44,6 +44,7 @@ const oauthAccountNoun = {
44
44
  [GQLIntegration.GoogleAnalytics]: "Property",
45
45
  [GQLIntegration.Meta]: "Ad account",
46
46
  [GQLIntegration.Linkedin]: "Ad account",
47
+ [GQLIntegration.MicrosoftAds]: "Account",
47
48
  [GQLIntegration.Snapchat]: "Ad account",
48
49
  [GQLIntegration.Shopify]: "Shop",
49
50
  [GQLIntegration.Woocommerce]: "Store",