@manta-eu/react 0.5.1 → 0.7.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/button/index.d.ts +1 -0
  2. package/dist/components/button/index.d.ts.map +1 -1
  3. package/dist/components/button/index.js +17 -1
  4. package/dist/components/button/index.js.map +1 -1
  5. package/dist/components/chat-view/ToolApprovalPart.d.ts +12 -0
  6. package/dist/components/chat-view/ToolApprovalPart.d.ts.map +1 -0
  7. package/dist/components/chat-view/ToolApprovalPart.js +12 -0
  8. package/dist/components/chat-view/ToolApprovalPart.js.map +1 -0
  9. package/dist/components/chat-view/ToolMessagePart.d.ts.map +1 -1
  10. package/dist/components/chat-view/ToolMessagePart.js +7 -15
  11. package/dist/components/chat-view/ToolMessagePart.js.map +1 -1
  12. package/dist/components/chat-view/tool-part-rendering.js +1 -1
  13. package/dist/components/chat-view/tool-part-rendering.js.map +1 -1
  14. package/dist/components/modal/index.d.ts +4 -0
  15. package/dist/components/modal/index.d.ts.map +1 -1
  16. package/dist/components/modal/index.js +11 -3
  17. package/dist/components/modal/index.js.map +1 -1
  18. package/dist/components/planning/ActionPlanDetailView.d.ts +1 -0
  19. package/dist/components/planning/ActionPlanDetailView.d.ts.map +1 -1
  20. package/dist/components/planning/ActionPlanDetailView.js +2 -2
  21. package/dist/components/planning/ActionPlanDetailView.js.map +1 -1
  22. package/dist/components/planning/ConnectedPlanDetailView.d.ts +1 -0
  23. package/dist/components/planning/ConnectedPlanDetailView.d.ts.map +1 -1
  24. package/dist/components/planning/ConnectedPlanDetailView.js.map +1 -1
  25. package/dist/components/planning/TopBar.d.ts +1 -0
  26. package/dist/components/planning/TopBar.d.ts.map +1 -1
  27. package/dist/components/planning/TopBar.js +7 -4
  28. package/dist/components/planning/TopBar.js.map +1 -1
  29. package/dist/components/planning/plan-detail-utils.d.ts +1 -0
  30. package/dist/components/planning/plan-detail-utils.d.ts.map +1 -1
  31. package/dist/components/planning/plan-detail-utils.js +4 -1
  32. package/dist/components/planning/plan-detail-utils.js.map +1 -1
  33. package/dist/components/sheet/index.d.ts +13 -19
  34. package/dist/components/sheet/index.d.ts.map +1 -1
  35. package/dist/components/sheet/index.js +8 -2
  36. package/dist/components/sheet/index.js.map +1 -1
  37. package/dist/components/table/index.d.ts +7 -1
  38. package/dist/components/table/index.d.ts.map +1 -1
  39. package/dist/components/table/index.js +18 -12
  40. package/dist/components/table/index.js.map +1 -1
  41. package/dist/components/tool-invocation-card/ToolActions.d.ts +2 -4
  42. package/dist/components/tool-invocation-card/ToolActions.d.ts.map +1 -1
  43. package/dist/components/tool-invocation-card/ToolActions.js +2 -9
  44. package/dist/components/tool-invocation-card/ToolActions.js.map +1 -1
  45. package/dist/components/tool-invocation-card/ToolHeader.d.ts +0 -1
  46. package/dist/components/tool-invocation-card/ToolHeader.d.ts.map +1 -1
  47. package/dist/components/tool-invocation-card/ToolHeader.js +11 -4
  48. package/dist/components/tool-invocation-card/ToolHeader.js.map +1 -1
  49. package/dist/components/tool-invocation-card/ToolInvocationCard.d.ts +0 -7
  50. package/dist/components/tool-invocation-card/ToolInvocationCard.d.ts.map +1 -1
  51. package/dist/components/tool-invocation-card/ToolInvocationCard.js +6 -8
  52. package/dist/components/tool-invocation-card/ToolInvocationCard.js.map +1 -1
  53. package/dist/globals.css +1526 -2772
  54. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  55. package/dist/hooks/useActionGraph.d.ts +7 -0
  56. package/dist/hooks/useActionGraph.d.ts.map +1 -0
  57. package/dist/hooks/useActionGraph.js +23 -0
  58. package/dist/hooks/useActionGraph.js.map +1 -0
  59. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  60. package/dist/lib/tool-icons.d.ts.map +1 -1
  61. package/dist/lib/tool-icons.js +1 -0
  62. package/dist/lib/tool-icons.js.map +1 -1
  63. package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.d.ts +1 -1
  64. package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.d.ts.map +1 -1
  65. package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.js +64 -11
  66. package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.js.map +1 -1
  67. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.d.ts.map +1 -1
  68. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js +8 -2
  69. package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js.map +1 -1
  70. package/dist/routes/integrations/components/connect-forms/meta-page-tree.d.ts +5 -0
  71. package/dist/routes/integrations/components/connect-forms/meta-page-tree.d.ts.map +1 -0
  72. package/dist/routes/integrations/components/connect-forms/meta-page-tree.js +16 -0
  73. package/dist/routes/integrations/components/connect-forms/meta-page-tree.js.map +1 -0
  74. package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.d.ts +2 -1
  75. package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.d.ts.map +1 -1
  76. package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.js +4 -1
  77. package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.js.map +1 -1
  78. package/dist/routes/integrations/connection-view.d.ts.map +1 -1
  79. package/dist/routes/integrations/connection-view.js +1 -0
  80. package/dist/routes/integrations/connection-view.js.map +1 -1
  81. package/dist/routes/plans/components/ActionPlansPage.d.ts.map +1 -1
  82. package/dist/routes/plans/components/ActionPlansPage.js +3 -3
  83. package/dist/routes/plans/components/ActionPlansPage.js.map +1 -1
  84. package/dist/routes/plans/components/PlanDetailRouteContent.d.ts.map +1 -1
  85. package/dist/routes/plans/components/PlanDetailRouteContent.js +3 -1
  86. package/dist/routes/plans/components/PlanDetailRouteContent.js.map +1 -1
  87. package/package.json +20 -15
  88. package/src/app-subpaths.json +1 -0
  89. package/src/components/button/index.tsx +17 -1
  90. package/src/components/chat-view/ToolApprovalPart.tsx +27 -0
  91. package/src/components/chat-view/ToolMessagePart.tsx +8 -25
  92. package/src/components/chat-view/tool-part-rendering.ts +1 -1
  93. package/src/components/modal/index.tsx +17 -2
  94. package/src/components/planning/ActionPlanDetailView.tsx +3 -0
  95. package/src/components/planning/ConnectedPlanDetailView.tsx +1 -0
  96. package/src/components/planning/TopBar.tsx +14 -3
  97. package/src/components/planning/plan-detail-utils.tsx +5 -1
  98. package/src/components/sheet/index.tsx +29 -6
  99. package/src/components/table/index.tsx +42 -12
  100. package/src/components/tool-invocation-card/ToolActions.tsx +5 -16
  101. package/src/components/tool-invocation-card/ToolHeader.tsx +13 -6
  102. package/src/components/tool-invocation-card/ToolInvocationCard.tsx +6 -23
  103. package/src/hooks/useActionGraph.ts +41 -0
  104. package/src/lib/tool-icons.ts +1 -0
  105. package/src/routes/integrations/components/connect-forms/MetaAdsConnectForm.tsx +131 -15
  106. package/src/routes/integrations/components/connect-forms/OAuthSetupConnectForm.tsx +8 -2
  107. package/src/routes/integrations/components/connect-forms/meta-page-tree.ts +17 -0
  108. package/src/routes/integrations/components/connect-forms/oauth-setup-phase.ts +6 -0
  109. package/src/routes/integrations/connection-view.ts +1 -0
  110. package/src/routes/plans/components/ActionPlansPage.tsx +4 -4
  111. package/src/routes/plans/components/PlanDetailRouteContent.tsx +3 -0
@@ -4,7 +4,7 @@ import type { Cell, Column, ColumnDef, Row, SortingState, Table as TableInstance
4
4
  import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
5
5
  import { ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react";
6
6
  import type { ComponentProps, HTMLAttributes, KeyboardEvent, ReactNode, RefObject } from "react";
7
- import { useMemo, useState } from "react";
7
+ import { createContext, useContext, useMemo, useState } from "react";
8
8
  import { match } from "ts-pattern";
9
9
  import { getCommonPinnedColumnStyles } from "./table-utils";
10
10
  import { Button } from "#components/button/index";
@@ -18,11 +18,26 @@ export { getCommonPinnedColumnStyles, getPinnedColumnInfo, type CombinedSortOpti
18
18
  export { useMaxTableRows } from "./use-max-table-rows";
19
19
  export { useTableState, type UseTableStateOptions } from "./use-table-state";
20
20
 
21
- export function Table({ className, ...props }: ComponentProps<"table">) {
21
+ export type TableSize = "base" | "sm";
22
+
23
+ const TableSizeContext = createContext<TableSize>("base");
24
+
25
+ export type TableProps = ComponentProps<"table"> & {
26
+ size?: TableSize;
27
+ };
28
+
29
+ export function Table({ className, size = "base", ...props }: TableProps) {
22
30
  return (
23
- <div data-slot="table-container" className="relative w-full overflow-x-auto">
24
- <table data-slot="table" className={cn("w-full caption-bottom typography-small", className)} {...props} />
25
- </div>
31
+ <TableSizeContext.Provider value={size}>
32
+ <div data-slot="table-container" className="relative w-full overflow-x-auto">
33
+ <table
34
+ data-slot="table"
35
+ data-size={size}
36
+ className={cn("w-full caption-bottom typography-small", className)}
37
+ {...props}
38
+ />
39
+ </div>
40
+ </TableSizeContext.Provider>
26
41
  );
27
42
  }
28
43
 
@@ -45,11 +60,13 @@ export function TableFooter({ className, ...props }: ComponentProps<"tfoot">) {
45
60
  }
46
61
 
47
62
  export function TableRow({ className, ...props }: ComponentProps<"tr">) {
63
+ const size = useContext(TableSizeContext);
48
64
  return (
49
65
  <tr
50
66
  data-slot="table-row"
51
67
  className={cn(
52
68
  "bg-card hover:bg-muted data-[state=selected]:bg-muted border-b transition-colors text-default-foreground [&>td[data-is-pinned]]:bg-card [&>td[data-is-pinned]]:transition-colors [&>td[data-is-pinned]]:group-hover:bg-muted",
69
+ size === "sm" && "border-default-border",
53
70
  className,
54
71
  )}
55
72
  {...props}
@@ -58,11 +75,13 @@ export function TableRow({ className, ...props }: ComponentProps<"tr">) {
58
75
  }
59
76
 
60
77
  export function TableHead({ className, ...props }: ComponentProps<"th">) {
78
+ const size = useContext(TableSizeContext);
61
79
  return (
62
80
  <th
63
81
  data-slot="table-head"
64
82
  className={cn(
65
- "bg-faint text-muted-foreground h-10 px-2 text-left align-middle typography-small-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] data-is-pinned:bg-faint",
83
+ "bg-faint text-muted-foreground h-10 px-2 text-left align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] data-is-pinned:bg-faint",
84
+ size === "sm" ? "h-9 px-4 py-2.5 typography-tiny-medium whitespace-normal" : "typography-small-medium",
66
85
  className,
67
86
  )}
68
87
  {...props}
@@ -71,11 +90,13 @@ export function TableHead({ className, ...props }: ComponentProps<"th">) {
71
90
  }
72
91
 
73
92
  export function TableCell({ className, ...props }: ComponentProps<"td">) {
93
+ const size = useContext(TableSizeContext);
74
94
  return (
75
95
  <td
76
96
  data-slot="table-cell"
77
97
  className={cn(
78
98
  "px-4 not-first:px-3 py-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
99
+ size === "sm" && "not-first:px-4 py-3 align-top text-[0.8125rem] leading-5 whitespace-normal break-words",
79
100
  className,
80
101
  )}
81
102
  {...props}
@@ -117,13 +138,14 @@ declare module "@tanstack/react-table" {
117
138
 
118
139
  export function CustomTableCell<TData, TValue>({ cell }: { cell: Cell<TData, TValue> }) {
119
140
  const columnType = cell.column.columnDef.meta?.type;
141
+ const size = useContext(TableSizeContext);
120
142
 
121
143
  const pinningStyles = getCommonPinnedColumnStyles(cell.column);
122
144
 
123
145
  return (
124
146
  <TableCell
125
147
  className={cn(
126
- "h-12",
148
+ size === "base" && "h-12",
127
149
  cell.column.columnDef.meta?.wide ? "max-w-none" : "max-w-56",
128
150
  cell.column.columnDef.meta?.className,
129
151
  )}
@@ -159,7 +181,7 @@ export function ColumnHeader<TData, TValue>({ column, children }: TColumnHeaderP
159
181
  const isSorted = column.getIsSorted();
160
182
 
161
183
  const className = cn(
162
- "typography-caption-medium flex gap-0.5 text-muted-foreground items-center group/sort-header",
184
+ "flex gap-0.5 text-muted-foreground items-center group/sort-header",
163
185
  isNumberColumn && "justify-end",
164
186
  column.getCanSort() && "cursor-pointer",
165
187
  );
@@ -203,6 +225,7 @@ export type DataTableBaseProps<TData> = HTMLAttributes<HTMLDivElement> & {
203
225
  renderRow?: (row: Row<TData>) => ReactNode;
204
226
  rowClassName?: (row: Row<TData>) => string | undefined;
205
227
  isRowClickable?: (row: Row<TData>) => boolean;
228
+ size?: TableSize;
206
229
  };
207
230
 
208
231
  export type DataTableWithRowClick<TData> = DataTableBaseProps<TData> & {
@@ -258,6 +281,7 @@ export function DataTable<TData>(props: DataTableProps<TData>) {
258
281
  renderRow,
259
282
  rowClassName,
260
283
  isRowClickable,
284
+ size = "base",
261
285
  ...restProps
262
286
  } = props;
263
287
 
@@ -265,7 +289,9 @@ export function DataTable<TData>(props: DataTableProps<TData>) {
265
289
 
266
290
  const hasClickHandler = !!onRowClick || !!rowHref;
267
291
 
268
- const tableBodyRowClassName = cn("group border-muted-border bg-card py-2 px-4", hasClickHandler && "cursor-pointer");
292
+ const rowBorderClassName = size === "base" && "border-muted-border";
293
+
294
+ const tableBodyRowClassName = cn("group bg-card py-2 px-4", rowBorderClassName, hasClickHandler && "cursor-pointer");
269
295
 
270
296
  const handleRowClick = (row: Row<TData>) => {
271
297
  if (onRowClick) {
@@ -329,10 +355,10 @@ export function DataTable<TData>(props: DataTableProps<TData>) {
329
355
 
330
356
  return (
331
357
  <div className={cn("grid gap-2.5", className)} {...restProps} ref={ref}>
332
- <Table>
358
+ <Table size={size}>
333
359
  <TableHeader className={headerClassName}>
334
360
  {table.getHeaderGroups().map((headerGroup) => (
335
- <TableRow key={headerGroup.id} className="border-muted-border">
361
+ <TableRow key={headerGroup.id} className={cn(rowBorderClassName)}>
336
362
  {headerGroup.headers.map((header) => {
337
363
  const headerContent = header.isPlaceholder
338
364
  ? null
@@ -343,7 +369,10 @@ export function DataTable<TData>(props: DataTableProps<TData>) {
343
369
  return (
344
370
  <TableHead
345
371
  key={header.id}
346
- className={cn("h-13 px-4 not-first:px-3 py-2", header.column.columnDef.meta?.className)}
372
+ className={cn(
373
+ size === "base" && "h-13 px-4 not-first:px-3 py-2",
374
+ header.column.columnDef.meta?.className,
375
+ )}
347
376
  data-is-pinned={header.column.getIsPinned() ? true : undefined}
348
377
  style={pinningStyles}
349
378
  ref={(thElement) => columnSizingHandler(thElement, table, header.column)}
@@ -390,6 +419,7 @@ export type SortableTableBaseProps<TRow> = {
390
419
  noResultsMessage?: string;
391
420
  className?: string;
392
421
  isLoading?: boolean;
422
+ size?: TableSize;
393
423
  };
394
424
 
395
425
  export type SortableTableProps<TRow> =
@@ -1,30 +1,19 @@
1
- import { APPROVAL } from "@manta-eu/client/shared/index";
2
- import { type ToolInvocationCardProps } from "./ToolInvocationCard";
3
1
  import { Button } from "#components/button/index";
4
2
 
5
3
  type ToolActionsProps = {
6
- toolCallId: string;
7
- toolName: string;
8
- onSubmit: ToolInvocationCardProps["onSubmit"];
4
+ onApprove: () => void;
5
+ onReject: () => void;
9
6
  };
10
7
 
11
8
  export function ToolActions(props: ToolActionsProps) {
12
- const { toolCallId, toolName: _toolName, onSubmit } = props;
13
-
14
- function handleReject() {
15
- onSubmit({ toolCallId, result: APPROVAL.NO });
16
- }
17
-
18
- function handleApprove() {
19
- onSubmit({ toolCallId, result: APPROVAL.YES });
20
- }
9
+ const { onApprove, onReject } = props;
21
10
 
22
11
  return (
23
12
  <div className="flex gap-2 justify-end">
24
- <Button variant="destructive" size="sm" onClick={handleReject}>
13
+ <Button variant="destructive" size="sm" onClick={onReject}>
25
14
  Reject
26
15
  </Button>
27
- <Button size="sm" onClick={handleApprove}>
16
+ <Button size="sm" onClick={onApprove}>
28
17
  Approve
29
18
  </Button>
30
19
  </div>
@@ -1,18 +1,24 @@
1
1
  import { getToolUIPartLabel } from "@manta-eu/client/shared/tools";
2
2
  import type { ToolUIPart } from "ai";
3
- import { CheckCircle2, ChevronDown, Loader2 } from "lucide-react";
3
+ import { AlertTriangle, CheckCircle2, ChevronDown, Loader2, XCircle } from "lucide-react";
4
+ import { match } from "ts-pattern";
4
5
 
5
6
  type ToolHeaderProps = {
6
7
  toolUIPart: ToolUIPart;
7
- needsConfirmation: boolean;
8
8
  isExpanded: boolean;
9
9
  onToggle: () => void;
10
10
  };
11
11
 
12
- export function ToolHeader(props: ToolHeaderProps) {
13
- const { toolUIPart, isExpanded, onToggle, needsConfirmation } = props;
12
+ function StateIcon(props: { state: ToolUIPart["state"] }) {
13
+ return match(props.state)
14
+ .with("output-available", () => <CheckCircle2 size={16} />)
15
+ .with("output-denied", () => <XCircle size={16} />)
16
+ .with("output-error", () => <AlertTriangle size={16} />)
17
+ .otherwise(() => <Loader2 size={16} className="animate-spin" />);
18
+ }
14
19
 
15
- const isComplete = !needsConfirmation && toolUIPart.state === "output-available";
20
+ export function ToolHeader(props: ToolHeaderProps) {
21
+ const { toolUIPart, isExpanded, onToggle } = props;
16
22
 
17
23
  const humanToolName = getToolUIPartLabel(toolUIPart);
18
24
 
@@ -23,8 +29,9 @@ export function ToolHeader(props: ToolHeaderProps) {
23
29
  return (
24
30
  <button type="button" onClick={handleToggle} className="w-full flex items-center gap-2 cursor-pointer">
25
31
  <h4 className="typography-small-medium flex items-center gap-2 flex-1 text-left">
26
- {isComplete ? <CheckCircle2 size={16} /> : <Loader2 size={16} className="animate-spin" />}
32
+ <StateIcon state={toolUIPart.state} />
27
33
  {humanToolName}
34
+ {toolUIPart.state === "output-denied" && <span className="text-muted-foreground">Rejected</span>}
28
35
  </h4>
29
36
  <ChevronDown
30
37
  size={16}
@@ -2,7 +2,6 @@ import { getToolUIPartLabel } from "@manta-eu/client/shared/tools";
2
2
  import type { ToolUIPart } from "ai";
3
3
  import { useCallback, useState } from "react";
4
4
  import { match } from "ts-pattern";
5
- import { ToolActions } from "./ToolActions";
6
5
  import { ToolArgumentsDisplay } from "./ToolArgumentsDisplay";
7
6
  import { ToolHeader } from "./ToolHeader";
8
7
  import { ToolLoadingState } from "./ToolLoadingState";
@@ -12,30 +11,21 @@ import { cn } from "#lib/utils";
12
11
 
13
12
  export type ToolInvocationCardProps = {
14
13
  toolUIPart: ToolUIPart;
15
- toolCallId: string;
16
- needsConfirmation: boolean;
17
- onSubmit: ({ toolCallId, result }: { toolCallId: string; result: string }) => void;
18
- addToolResult: (toolCallId: string, result: string) => void;
19
14
  };
20
15
 
21
16
  export function ToolInvocationCard(props: ToolInvocationCardProps) {
22
- const { toolUIPart, toolCallId, needsConfirmation, onSubmit } = props;
17
+ const { toolUIPart } = props;
23
18
  const [isExpanded, setIsExpanded] = useState(false);
24
19
  const loadingLabel = getToolUIPartLabel(toolUIPart);
25
20
  const [executionStartTime] = useState(Date.now());
26
21
 
27
22
  const handleToggle = useCallback(() => {
28
- setIsExpanded(!isExpanded);
23
+ setIsExpanded((expanded) => !expanded);
29
24
  }, []);
30
25
 
31
26
  return (
32
27
  <Card className="my-3 w-full max-w-2xl card-base overflow-hidden">
33
- <ToolHeader
34
- toolUIPart={toolUIPart}
35
- needsConfirmation={needsConfirmation}
36
- isExpanded={isExpanded}
37
- onToggle={handleToggle}
38
- />
28
+ <ToolHeader toolUIPart={toolUIPart} isExpanded={isExpanded} onToggle={handleToggle} />
39
29
 
40
30
  <div
41
31
  className={cn([
@@ -46,22 +36,15 @@ export function ToolInvocationCard(props: ToolInvocationCardProps) {
46
36
  <div className="overflow-y-auto" style={{ maxHeight: isExpanded ? "580px" : "0px" }}>
47
37
  <ToolArgumentsDisplay toolUIPart={toolUIPart} />
48
38
 
49
- {match({ needsConfirmation, state: toolUIPart.state })
50
- .with({ needsConfirmation: true, state: "input-available" }, () => (
51
- <ToolActions
52
- toolCallId={toolCallId}
53
- toolName={toolUIPart.type.replace("tool-", "")}
54
- onSubmit={onSubmit}
55
- />
56
- ))
57
- .with({ needsConfirmation: false, state: "output-available" }, () => (
39
+ {match(toolUIPart.state)
40
+ .with("output-available", () => (
58
41
  <ToolResultDisplay
59
42
  result={toolUIPart.output}
60
43
  executionStartTime={executionStartTime}
61
44
  toolUIPart={toolUIPart}
62
45
  />
63
46
  ))
64
- .with({ state: "input-streaming" }, () => <ToolLoadingState label={loadingLabel} />)
47
+ .with("input-streaming", () => <ToolLoadingState label={loadingLabel} />)
65
48
  .otherwise(() => null)}
66
49
  </div>
67
50
  </div>
@@ -0,0 +1,41 @@
1
+ import {
2
+ runActionGraph,
3
+ type ActionGraphql,
4
+ type GraphOp,
5
+ type GraphRun,
6
+ type GraphRunOptions,
7
+ } from "@manta-eu/client/shared/action-graph";
8
+ import { useMutation, useQueryClient, type UseMutationResult } from "@tanstack/react-query";
9
+ import { uniq } from "ramda";
10
+ import { useSparqlOptions } from "#hooks/use-sparql-options";
11
+ import { useMantaClient } from "#providers/MantaClientProvider";
12
+ import { invalidateActionPlanQueries } from "#shared/query-keys";
13
+
14
+ export type UseActionGraphOptions = Omit<GraphRunOptions, "graphql" | "readOptions">;
15
+
16
+ export const actionGraphMutationKey = ["ExecuteActionGraph"] as const;
17
+
18
+ const planGraphIris = ({ results }: GraphRun): string[] =>
19
+ uniq(results.flatMap((result) => ("planGraphIri" in result ? [result.planGraphIri] : [])));
20
+
21
+ /** Sends a change of several entities as one plan and reads back what each op did; `results[i]` is `ops[i]`. */
22
+ export function useActionGraph(
23
+ options: UseActionGraphOptions = {},
24
+ ): UseMutationResult<GraphRun, Error, readonly GraphOp[]> {
25
+ const { mantaClient } = useMantaClient();
26
+ const readOptions = useSparqlOptions();
27
+ const queryClient = useQueryClient();
28
+
29
+ const graphql: ActionGraphql = (document, variables, request) =>
30
+ mantaClient.request({ document, variables, signal: request?.signal });
31
+
32
+ return useMutation<GraphRun, Error, readonly GraphOp[]>({
33
+ mutationKey: actionGraphMutationKey,
34
+ mutationFn: (ops) => runActionGraph(ops, { ...options, graphql, readOptions }),
35
+ onSuccess: async (run) => {
36
+ await Promise.all(
37
+ planGraphIris(run).map((planGraphIri) => invalidateActionPlanQueries(queryClient, planGraphIri)),
38
+ );
39
+ },
40
+ });
41
+ }
@@ -44,6 +44,7 @@ const toolIconMap: Record<ToolName, LucideIcon> = {
44
44
  setDailyChangeLimit: DollarSign,
45
45
  markAsLowRisk: CheckCircle2,
46
46
  readUploadedFile: Paperclip,
47
+ requestBudgetChange: DollarSign,
47
48
  };
48
49
 
49
50
  type StepIconName =
@@ -3,8 +3,11 @@ import {
3
3
  GQLMetaSetupStateEnum,
4
4
  useConnectedAccountsQuery,
5
5
  useMetaSetupConnectMutation,
6
+ useMetaSetupIncludePagesMutation,
6
7
  useMetaSetupProgressQuery,
7
8
  type GQLMetaAdAccount,
9
+ type GQLMetaPage,
10
+ type GQLMetaSetupProgress,
8
11
  } from "@manta-eu/client";
9
12
  import { parseError } from "@manta-eu/client/shared/error-utils";
10
13
  import { isPresent } from "@manta-eu/lib/present";
@@ -15,6 +18,7 @@ import { toast } from "sonner";
15
18
  import { match } from "ts-pattern";
16
19
  import { ConnectFormHeader } from "./ConnectFormHeader";
17
20
  import { ImportStartedNotice } from "./ImportStartedNotice";
21
+ import { metaPageTreeItems, picksAPage } from "./meta-page-tree";
18
22
  import { OAuthConnectLink } from "./OAuthConnectLink";
19
23
  import { unavailableReasonLabel } from "./setup-account-unavailable-reason";
20
24
  import type { ConnectFormProps, ImportNotice } from "./types";
@@ -24,12 +28,22 @@ import { Banner } from "#components/banner/index";
24
28
  import { Button } from "#components/button/index";
25
29
  import { Loader } from "#components/loader/index";
26
30
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "#components/select/index";
31
+ import { TreeMultiSelect } from "#components/tree-multi-select/index";
27
32
  import { cn } from "#lib/utils";
28
33
  import { useMantaClient } from "#providers/index";
29
34
 
30
35
  const pollIntervalMs = 3000;
31
36
 
32
- type Phase = "idle" | "awaitingOAuth" | "loadingAccounts" | "pickingAccount" | "submitting" | "done" | "failed";
37
+ type Phase =
38
+ | "idle"
39
+ | "awaitingOAuth"
40
+ | "loadingAccounts"
41
+ | "pickingAccount"
42
+ | "loadingPages"
43
+ | "pickingPages"
44
+ | "submitting"
45
+ | "done"
46
+ | "failed";
33
47
 
34
48
  type DerivePhaseInput = {
35
49
  states: GQLMetaSetupStateEnum[] | undefined;
@@ -42,6 +56,8 @@ const statePriority: readonly GQLMetaSetupStateEnum[] = [
42
56
  GQLMetaSetupStateEnum.FatallyFailed,
43
57
  GQLMetaSetupStateEnum.TimedOut,
44
58
  GQLMetaSetupStateEnum.ConnectingAccounts,
59
+ GQLMetaSetupStateEnum.SelectingPages,
60
+ GQLMetaSetupStateEnum.FetchingPages,
45
61
  GQLMetaSetupStateEnum.SelectingAccounts,
46
62
  GQLMetaSetupStateEnum.FetchingAccounts,
47
63
  GQLMetaSetupStateEnum.Setup,
@@ -53,7 +69,14 @@ const terminalStates: readonly GQLMetaSetupStateEnum[] = [
53
69
  GQLMetaSetupStateEnum.TimedOut,
54
70
  ];
55
71
 
56
- const activePhases: readonly Phase[] = ["awaitingOAuth", "loadingAccounts", "pickingAccount", "submitting"];
72
+ const activePhases: readonly Phase[] = [
73
+ "awaitingOAuth",
74
+ "loadingAccounts",
75
+ "pickingAccount",
76
+ "loadingPages",
77
+ "pickingPages",
78
+ "submitting",
79
+ ];
57
80
 
58
81
  export function derivePhase({ states, hasStartedOAuth, hasSeenActiveState }: DerivePhaseInput): Phase {
59
82
  const defaultPhase: Phase = hasStartedOAuth ? "awaitingOAuth" : "idle";
@@ -64,6 +87,8 @@ export function derivePhase({ states, hasStartedOAuth, hasSeenActiveState }: Der
64
87
  .with(GQLMetaSetupStateEnum.Finished, () => "done")
65
88
  .with(GQLMetaSetupStateEnum.FatallyFailed, GQLMetaSetupStateEnum.TimedOut, () => "failed")
66
89
  .with(GQLMetaSetupStateEnum.ConnectingAccounts, () => "submitting")
90
+ .with(GQLMetaSetupStateEnum.SelectingPages, () => "pickingPages")
91
+ .with(GQLMetaSetupStateEnum.FetchingPages, () => "loadingPages")
67
92
  .with(GQLMetaSetupStateEnum.SelectingAccounts, () => "pickingAccount")
68
93
  .with(GQLMetaSetupStateEnum.FetchingAccounts, GQLMetaSetupStateEnum.Setup, () => "loadingAccounts")
69
94
  .otherwise(() => defaultPhase);
@@ -114,11 +139,20 @@ export function MetaAdsConnectForm(props: ConnectFormProps) {
114
139
  const [hasStartedOAuth, setHasStartedOAuth] = useState(resumedFromOAuth);
115
140
  const [hasSeenActiveState, setHasSeenActiveState] = useState(resumedFromOAuth);
116
141
  const [selectedAccountId, setSelectedAccountId] = useState<string>("");
142
+ const [selectedPageIds, setSelectedPageIds] = useState<ReadonlySet<string>>(new Set());
117
143
  const hasDismissedRef = useRef(false);
118
144
 
119
145
  const progressQuery = useMetaSetupProgressQuery(mantaClient, undefined, { staleTime: 0 });
120
146
  const progress = progressQuery.data?.metaSetupProgress;
121
- const phase = derivePhase({ states: progress?.state, hasStartedOAuth, hasSeenActiveState });
147
+ const connectMutation = useMetaSetupConnectMutation(mantaClient);
148
+ const includePagesMutation = useMetaSetupIncludePagesMutation(mantaClient);
149
+ // Progress reads the old step until a mutation's step has run, so a pending one shows where it is heading.
150
+ const phase = connectMutation.isPending
151
+ ? "loadingPages"
152
+ : includePagesMutation.isPending
153
+ ? "submitting"
154
+ : derivePhase({ states: progress?.state, hasStartedOAuth, hasSeenActiveState });
155
+ const pages = progress?.pages ?? [];
122
156
 
123
157
  useWindowFocusPoll(progressQuery.refetch, { enabled: activePhases.includes(phase), intervalMs: pollIntervalMs });
124
158
 
@@ -139,8 +173,6 @@ export function MetaAdsConnectForm(props: ConnectFormProps) {
139
173
  );
140
174
  const reconnectableExternalIds = useReconnectableExternalIds(GQLIntegration.Meta);
141
175
 
142
- const connectMutation = useMetaSetupConnectMutation(mantaClient);
143
-
144
176
  useEffect(() => {
145
177
  if (phase !== "done" || hasDismissedRef.current) return;
146
178
  hasDismissedRef.current = true;
@@ -149,10 +181,12 @@ export function MetaAdsConnectForm(props: ConnectFormProps) {
149
181
  if (!importNotice) onDismiss();
150
182
  }, [phase, onChanged, onDismiss, importNotice]);
151
183
 
184
+ function setProgress(metaSetupProgress: GQLMetaSetupProgress) {
185
+ queryClient.setQueryData(useMetaSetupProgressQuery.getKey(), { metaSetupProgress });
186
+ }
187
+
152
188
  function resetProgressCache() {
153
- queryClient.setQueryData(useMetaSetupProgressQuery.getKey(), {
154
- metaSetupProgress: { state: [], accounts: null, lastError: null },
155
- });
189
+ setProgress({ state: [], accounts: null, pages: null, lastError: null });
156
190
  }
157
191
 
158
192
  function handleOpened() {
@@ -164,16 +198,34 @@ export function MetaAdsConnectForm(props: ConnectFormProps) {
164
198
  async function handleSubmit() {
165
199
  if (!selectedAccountId) return;
166
200
  try {
167
- await connectMutation.mutateAsync({ accountIds: [selectedAccountId] });
168
- await progressQuery.refetch();
201
+ setProgress((await connectMutation.mutateAsync({ accountIds: [selectedAccountId] })).metaSetupConnect);
169
202
  } catch (err) {
170
203
  const info = parseError(err, "Failed to connect Meta");
171
204
  toast.error(info.message, { description: info.description });
172
205
  }
173
206
  }
174
207
 
208
+ async function handleIncludePages() {
209
+ try {
210
+ setProgress((await includePagesMutation.mutateAsync({ pageIds: [...selectedPageIds] })).metaSetupIncludePages);
211
+ } catch (err) {
212
+ const info = parseError(err, "Failed to connect Meta");
213
+ toast.error(info.message, { description: info.description });
214
+ }
215
+ }
216
+
217
+ function togglePage(id: string, checked: boolean) {
218
+ setSelectedPageIds((current) => {
219
+ const next = new Set(current);
220
+ if (checked) next.add(id);
221
+ else next.delete(id);
222
+ return next;
223
+ });
224
+ }
225
+
175
226
  function handleRetry() {
176
227
  setSelectedAccountId("");
228
+ setSelectedPageIds(new Set());
177
229
  hasDismissedRef.current = false;
178
230
  resetProgressCache();
179
231
  setHasStartedOAuth(false);
@@ -198,6 +250,10 @@ export function MetaAdsConnectForm(props: ConnectFormProps) {
198
250
  onSelect={setSelectedAccountId}
199
251
  />
200
252
  ))
253
+ .with("loadingPages", () => <LoadingPagesView />)
254
+ .with("pickingPages", () => (
255
+ <PickingPagesView pages={pages} selectedIds={selectedPageIds} onToggle={togglePage} />
256
+ ))
201
257
  .with("submitting", () => <SubmittingView />)
202
258
  .with("done", () => <DoneView providerName={provider.name} importNotice={importNotice} />)
203
259
  .with("failed", () => (
@@ -220,12 +276,16 @@ export function MetaAdsConnectForm(props: ConnectFormProps) {
220
276
  <OAuthConnectLink href="/oauth/meta/start" label="Continue with Meta" onOpened={handleOpened} />
221
277
  )}
222
278
  {phase === "pickingAccount" && (
279
+ <Button onClick={() => void handleSubmit()} disabled={!selectedAccountId}>
280
+ Continue
281
+ </Button>
282
+ )}
283
+ {phase === "pickingPages" && (
223
284
  <Button
224
- onClick={() => void handleSubmit()}
225
- disabled={!selectedAccountId}
226
- isLoading={connectMutation.isPending}
285
+ onClick={() => void handleIncludePages()}
286
+ disabled={isPresent(pages) && !picksAPage(pages, selectedPageIds)}
227
287
  >
228
- Connect account
288
+ {isPresent(pages) ? "Connect" : "Connect without a Page"}
229
289
  </Button>
230
290
  )}
231
291
  {phase === "done" && importNotice && <Button onClick={onDismiss}>Done</Button>}
@@ -348,12 +408,68 @@ function PickingAccountView(props: PickingAccountViewProps) {
348
408
  );
349
409
  }
350
410
 
411
+ function LoadingPagesView() {
412
+ return (
413
+ <div className="grid gap-4">
414
+ <div className="grid gap-1">
415
+ <h2 className="typography-large-body-semibold text-default-foreground">Loading your Facebook Pages</h2>
416
+ <p className="typography-caption text-muted-foreground">
417
+ The Pages this ad account can advertise as, and their Instagram accounts.
418
+ </p>
419
+ </div>
420
+ <div
421
+ className="flex items-center gap-2 text-muted-foreground typography-caption"
422
+ role="status"
423
+ aria-live="polite"
424
+ >
425
+ <Loader className="size-4" aria-hidden /> Fetching Pages…
426
+ </div>
427
+ </div>
428
+ );
429
+ }
430
+
431
+ type PickingPagesViewProps = {
432
+ pages: GQLMetaPage[];
433
+ selectedIds: ReadonlySet<string>;
434
+ onToggle: (id: string, checked: boolean) => void;
435
+ };
436
+
437
+ function PickingPagesView(props: PickingPagesViewProps) {
438
+ const { pages, selectedIds, onToggle } = props;
439
+
440
+ if (!isPresent(pages)) {
441
+ return (
442
+ <Banner
443
+ type="warning"
444
+ label="This ad account can't advertise as any Facebook Page yet. Connect it anyway, and add a Page to it in Meta Business Suite before you run ads."
445
+ />
446
+ );
447
+ }
448
+
449
+ return (
450
+ <div className="grid gap-4">
451
+ <div className="grid gap-1">
452
+ <h2 className="typography-large-body-semibold text-default-foreground">Pick the Facebook Pages to use</h2>
453
+ <p className="typography-caption text-muted-foreground">
454
+ Manta runs ads only as the Pages and Instagram accounts you pick. Pick at least one Page.
455
+ </p>
456
+ </div>
457
+ <TreeMultiSelect
458
+ items={metaPageTreeItems(pages)}
459
+ selectedIds={selectedIds}
460
+ onToggle={onToggle}
461
+ searchPlaceholder="Search Pages..."
462
+ />
463
+ </div>
464
+ );
465
+ }
466
+
351
467
  function SubmittingView() {
352
468
  return (
353
469
  <div className="grid gap-4">
354
470
  <div className="grid gap-1">
355
471
  <h2 className="typography-large-body-semibold text-default-foreground">Connecting your account</h2>
356
- <p className="typography-caption text-muted-foreground">Hold tight — this usually takes a few seconds.</p>
472
+ <p className="typography-caption text-muted-foreground">This can take a minute. Keep this window open.</p>
357
473
  </div>
358
474
  <div
359
475
  className="flex items-center gap-2 text-muted-foreground typography-caption"