@manta-eu/react 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/button/index.d.ts +1 -0
- package/dist/components/button/index.d.ts.map +1 -1
- package/dist/components/button/index.js +17 -1
- package/dist/components/button/index.js.map +1 -1
- package/dist/components/chart/ForecastChart.d.ts +41 -0
- package/dist/components/chart/ForecastChart.d.ts.map +1 -0
- package/dist/components/chart/ForecastChart.js +110 -0
- package/dist/components/chart/ForecastChart.js.map +1 -0
- package/dist/components/chart/ForecastEventCard.d.ts +28 -0
- package/dist/components/chart/ForecastEventCard.d.ts.map +1 -0
- package/dist/components/chart/ForecastEventCard.js +56 -0
- package/dist/components/chart/ForecastEventCard.js.map +1 -0
- package/dist/components/chart/ForecastHoverCard.d.ts +23 -0
- package/dist/components/chart/ForecastHoverCard.d.ts.map +1 -0
- package/dist/components/chart/ForecastHoverCard.js +22 -0
- package/dist/components/chart/ForecastHoverCard.js.map +1 -0
- package/dist/components/chart/ForecastLegend.d.ts +33 -0
- package/dist/components/chart/ForecastLegend.d.ts.map +1 -0
- package/dist/components/chart/ForecastLegend.js +38 -0
- package/dist/components/chart/ForecastLegend.js.map +1 -0
- package/dist/components/chart/forecast-model.d.ts +70 -0
- package/dist/components/chart/forecast-model.d.ts.map +1 -0
- package/dist/components/chart/forecast-model.js +89 -0
- package/dist/components/chart/forecast-model.js.map +1 -0
- package/dist/components/chart/forecast.d.ts +62 -0
- package/dist/components/chart/forecast.d.ts.map +1 -0
- package/dist/components/chart/forecast.js +90 -0
- package/dist/components/chart/forecast.js.map +1 -0
- package/dist/components/chart/index.d.ts +3 -0
- package/dist/components/chart/index.d.ts.map +1 -1
- package/dist/components/chart/index.js +1 -0
- package/dist/components/chart/index.js.map +1 -1
- package/dist/components/modal/index.d.ts +4 -0
- package/dist/components/modal/index.d.ts.map +1 -1
- package/dist/components/modal/index.js +11 -3
- package/dist/components/modal/index.js.map +1 -1
- package/dist/components/popover/index.d.ts +2 -1
- package/dist/components/popover/index.d.ts.map +1 -1
- package/dist/components/popover/index.js +2 -1
- package/dist/components/popover/index.js.map +1 -1
- package/dist/components/sheet/index.d.ts +13 -19
- package/dist/components/sheet/index.d.ts.map +1 -1
- package/dist/components/sheet/index.js +8 -2
- package/dist/components/sheet/index.js.map +1 -1
- package/dist/components/table/index.d.ts +7 -1
- package/dist/components/table/index.d.ts.map +1 -1
- package/dist/components/table/index.js +18 -12
- package/dist/components/table/index.js.map +1 -1
- package/dist/globals.css +1582 -2770
- package/dist/hooks/use-connection-problems.d.ts.map +1 -1
- package/dist/hooks/useAuth.d.ts +1 -0
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useAuth.js +1 -0
- package/dist/hooks/useAuth.js.map +1 -1
- package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
- package/dist/providers/AuthContext.d.ts +1 -0
- package/dist/providers/AuthContext.d.ts.map +1 -1
- package/dist/providers/AuthContext.js.map +1 -1
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.d.ts +1 -1
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.js +64 -11
- package/dist/routes/integrations/components/connect-forms/MetaAdsConnectForm.js.map +1 -1
- package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js +8 -2
- package/dist/routes/integrations/components/connect-forms/OAuthSetupConnectForm.js.map +1 -1
- package/dist/routes/integrations/components/connect-forms/meta-page-tree.d.ts +5 -0
- package/dist/routes/integrations/components/connect-forms/meta-page-tree.d.ts.map +1 -0
- package/dist/routes/integrations/components/connect-forms/meta-page-tree.js +16 -0
- package/dist/routes/integrations/components/connect-forms/meta-page-tree.js.map +1 -0
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.d.ts +2 -1
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.d.ts.map +1 -1
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.js +4 -1
- package/dist/routes/integrations/components/connect-forms/oauth-setup-phase.js.map +1 -1
- package/dist/routes/integrations/connection-view.d.ts.map +1 -1
- package/dist/routes/integrations/connection-view.js +1 -0
- package/dist/routes/integrations/connection-view.js.map +1 -1
- package/package.json +16 -17
- package/src/components/button/index.tsx +17 -1
- package/src/components/chart/ForecastChart.tsx +383 -0
- package/src/components/chart/ForecastEventCard.tsx +163 -0
- package/src/components/chart/ForecastHoverCard.tsx +73 -0
- package/src/components/chart/ForecastLegend.tsx +97 -0
- package/src/components/chart/forecast-model.ts +130 -0
- package/src/components/chart/forecast.ts +144 -0
- package/src/components/chart/index.tsx +3 -0
- package/src/components/modal/index.tsx +17 -2
- package/src/components/popover/index.tsx +2 -1
- package/src/components/sheet/index.tsx +29 -6
- package/src/components/table/index.tsx +42 -12
- package/src/globals.css +2 -0
- package/src/hooks/useAuth.ts +2 -0
- package/src/providers/AuthContext.ts +1 -0
- package/src/routes/integrations/components/connect-forms/MetaAdsConnectForm.tsx +131 -15
- package/src/routes/integrations/components/connect-forms/OAuthSetupConnectForm.tsx +8 -2
- package/src/routes/integrations/components/connect-forms/meta-page-tree.ts +17 -0
- package/src/routes/integrations/components/connect-forms/oauth-setup-phase.ts +6 -0
- package/src/routes/integrations/connection-view.ts +1 -0
- package/src/themes/manta-dark.css +2 -0
- package/src/themes/manta.css +2 -0
- package/src/themes/standard.css +2 -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
|
|
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
|
-
<
|
|
24
|
-
<
|
|
25
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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=
|
|
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(
|
|
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> =
|
package/src/globals.css
CHANGED
|
@@ -381,6 +381,8 @@
|
|
|
381
381
|
--color-rank-3: #ffe4d6;
|
|
382
382
|
|
|
383
383
|
--color-default-foreground: var(--color-neutral-900);
|
|
384
|
+
--color-inverse: var(--color-neutral-900);
|
|
385
|
+
--color-inverse-foreground: #ffffff;
|
|
384
386
|
--color-body-foreground: rgba(23, 23, 23, 0.85);
|
|
385
387
|
--color-muted-foreground: var(--color-neutral-500);
|
|
386
388
|
--color-disabled-foreground: var(--color-neutral-400);
|
package/src/hooks/useAuth.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type UseAuthResult = {
|
|
|
22
22
|
user: User | undefined;
|
|
23
23
|
datastore: Datastore | undefined;
|
|
24
24
|
datastores: Datastore[] | undefined;
|
|
25
|
+
inOrganization: boolean;
|
|
25
26
|
isLoading: boolean;
|
|
26
27
|
isAuthenticated: boolean;
|
|
27
28
|
isOperator: boolean;
|
|
@@ -296,6 +297,7 @@ export function useAuth(): UseAuthResult {
|
|
|
296
297
|
const datastore = sessionDatastore(row);
|
|
297
298
|
return isPresent(datastore) ? [datastore] : [];
|
|
298
299
|
}),
|
|
300
|
+
inOrganization: isPresent(contextData?.user?.memberOrgs) || isPresent(contextData?.datastores),
|
|
299
301
|
datastoreAccessGranted: contextData?.datastoreAccessGranted ?? true,
|
|
300
302
|
isOperator: contextData?.user?.isOperator ?? false,
|
|
301
303
|
canManageTeam: contextData?.user?.canManageTeam ?? false,
|
|
@@ -21,6 +21,7 @@ export type AuthContextValue = Pick<
|
|
|
21
21
|
// Only Manta's own web app may list the apps connected to someone; any other host's token is refused.
|
|
22
22
|
canDisconnectCustomApps?: boolean;
|
|
23
23
|
hasProfilePage?: boolean;
|
|
24
|
+
inOrganization?: boolean;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
@@ -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 =
|
|
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[] = [
|
|
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
|
|
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
|
-
|
|
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
|
|
225
|
-
disabled={!
|
|
226
|
-
isLoading={connectMutation.isPending}
|
|
285
|
+
onClick={() => void handleIncludePages()}
|
|
286
|
+
disabled={isPresent(pages) && !picksAPage(pages, selectedPageIds)}
|
|
227
287
|
>
|
|
228
|
-
Connect
|
|
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">
|
|
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"
|
|
@@ -85,11 +85,12 @@ export function OAuthSetupConnectForm<Row extends SetupRow>(props: OAuthSetupCon
|
|
|
85
85
|
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(new Set());
|
|
86
86
|
const hasDismissedRef = useRef(false);
|
|
87
87
|
const [connectionExpired, setConnectionExpired] = useState(false);
|
|
88
|
+
const [submitted, setSubmitted] = useState(false);
|
|
88
89
|
|
|
89
90
|
const states = setup.progress?.state;
|
|
90
91
|
const phase = connectionExpired
|
|
91
92
|
? "failed"
|
|
92
|
-
: derivePhase({ states, hasStartedOAuth, hasSeenActiveState, resumesUnfinishedSetup });
|
|
93
|
+
: derivePhase({ states, hasStartedOAuth, hasSeenActiveState, resumesUnfinishedSetup, submitted });
|
|
93
94
|
|
|
94
95
|
useWindowFocusPoll(setup.refetchProgress, { enabled: activePhases.includes(phase), intervalMs: pollIntervalMs });
|
|
95
96
|
|
|
@@ -114,6 +115,7 @@ export function OAuthSetupConnectForm<Row extends SetupRow>(props: OAuthSetupCon
|
|
|
114
115
|
|
|
115
116
|
function handleOpened() {
|
|
116
117
|
setConnectionExpired(false);
|
|
118
|
+
setSubmitted(false);
|
|
117
119
|
setSelectedIds(new Set());
|
|
118
120
|
setup.resetProgress();
|
|
119
121
|
setHasSeenActiveState(false);
|
|
@@ -131,21 +133,25 @@ export function OAuthSetupConnectForm<Row extends SetupRow>(props: OAuthSetupCon
|
|
|
131
133
|
|
|
132
134
|
async function handleSubmit() {
|
|
133
135
|
if (selectedIds.size === 0) return;
|
|
136
|
+
setSubmitted(true);
|
|
134
137
|
try {
|
|
135
138
|
const result = await setup.connect([...selectedIds]);
|
|
136
139
|
if (result.state.includes(GQLGoogleSetupStateEnum.NotFound)) {
|
|
140
|
+
setSubmitted(false);
|
|
137
141
|
setConnectionExpired(true);
|
|
138
142
|
setSelectedIds(new Set());
|
|
139
143
|
return;
|
|
140
144
|
}
|
|
141
145
|
await setup.refetchProgress();
|
|
142
146
|
} catch (err) {
|
|
147
|
+
setSubmitted(false);
|
|
143
148
|
const info = parseError(err, `Failed to connect ${providerName}`);
|
|
144
149
|
toast.error(info.message, { description: info.description });
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
function handleRetry() {
|
|
154
|
+
setSubmitted(false);
|
|
149
155
|
setSelectedIds(new Set());
|
|
150
156
|
hasDismissedRef.current = false;
|
|
151
157
|
setup.resetProgress();
|
|
@@ -343,7 +349,7 @@ function SubmittingView(props: CopyProps) {
|
|
|
343
349
|
<div className="grid gap-4">
|
|
344
350
|
<div className="grid gap-1">
|
|
345
351
|
<h2 className="typography-large-body-semibold text-default-foreground">Connecting your {copy.entityPlural}</h2>
|
|
346
|
-
<p className="typography-caption text-muted-foreground">
|
|
352
|
+
<p className="typography-caption text-muted-foreground">This can take a minute. Keep this window open.</p>
|
|
347
353
|
</div>
|
|
348
354
|
<StatusLine label="Provisioning…" />
|
|
349
355
|
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { GQLMetaPageKind, type GQLMetaPage } from "@manta-eu/client";
|
|
2
|
+
import { buildTreeRows, type TreeSelectItem } from "#components/tree-multi-select/index";
|
|
3
|
+
|
|
4
|
+
// Each Instagram account sits under the Page it is linked to, and is picked on its own.
|
|
5
|
+
export function metaPageTreeItems(pages: readonly GQLMetaPage[]): TreeSelectItem[] {
|
|
6
|
+
return buildTreeRows(pages).map(({ node, depth }) => ({
|
|
7
|
+
id: node.id,
|
|
8
|
+
label: node.name ?? (node.kind === GQLMetaPageKind.Instagram ? `Instagram account ${node.id}` : node.id),
|
|
9
|
+
caption: node.kind === GQLMetaPageKind.Instagram ? "Instagram" : undefined,
|
|
10
|
+
selectable: true,
|
|
11
|
+
depth,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function picksAPage(pages: readonly GQLMetaPage[], selectedIds: ReadonlySet<string>): boolean {
|
|
16
|
+
return pages.some((page) => page.kind === GQLMetaPageKind.Page && selectedIds.has(page.id));
|
|
17
|
+
}
|
|
@@ -9,6 +9,8 @@ export type DerivePhaseInput = {
|
|
|
9
9
|
hasSeenActiveState: boolean;
|
|
10
10
|
// Off when the account is connected before its picker, so a leftover setup cannot stand in for a new sign-in.
|
|
11
11
|
resumesUnfinishedSetup?: boolean;
|
|
12
|
+
// Connect was sent: progress reads the old state until the connect has run, so only an outcome ends it.
|
|
13
|
+
submitted?: boolean;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
const statePriority: readonly GQLGoogleSetupStateEnum[] = [
|
|
@@ -34,10 +36,14 @@ export function derivePhase({
|
|
|
34
36
|
hasStartedOAuth,
|
|
35
37
|
hasSeenActiveState,
|
|
36
38
|
resumesUnfinishedSetup = true,
|
|
39
|
+
submitted = false,
|
|
37
40
|
}: DerivePhaseInput): Phase {
|
|
38
41
|
const defaultPhase: Phase = hasStartedOAuth ? "awaitingOAuth" : "idle";
|
|
39
42
|
if (!hasStartedOAuth && !resumesUnfinishedSetup) return defaultPhase;
|
|
40
43
|
const dominant = statePriority.find((state) => states?.includes(state));
|
|
44
|
+
const settled =
|
|
45
|
+
states?.includes(GQLGoogleSetupStateEnum.NotFound) || (dominant !== undefined && terminalStates.includes(dominant));
|
|
46
|
+
if (submitted && !settled) return "submitting";
|
|
41
47
|
if (dominant && terminalStates.includes(dominant) && !hasSeenActiveState) return defaultPhase;
|
|
42
48
|
return match(dominant)
|
|
43
49
|
.returnType<Phase>()
|
|
@@ -87,6 +87,7 @@ const grantExpiryWarningDays = 14;
|
|
|
87
87
|
// Integrations whose account is limited to what was picked at connect time, so an empty pick reads nothing.
|
|
88
88
|
const oauthIncludedResourcesLabels: Partial<Record<GQLIntegration, { label: string; noneLabel: string }>> = {
|
|
89
89
|
[GQLIntegration.Dash]: { label: "Folders", noneLabel: "No folders selected" },
|
|
90
|
+
[GQLIntegration.Meta]: { label: "Pages", noneLabel: "No Pages picked, reconnect to pick them" },
|
|
90
91
|
};
|
|
91
92
|
|
|
92
93
|
function timeAgoLabel(event: string, at: string | null | undefined): string | undefined {
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
--color-rank-3: #431407;
|
|
26
26
|
|
|
27
27
|
--color-default-foreground: #ffffff;
|
|
28
|
+
--color-inverse: #05070d;
|
|
29
|
+
--color-inverse-foreground: #ffffff;
|
|
28
30
|
--color-body-foreground: rgba(255, 255, 255, 0.8);
|
|
29
31
|
--color-muted-foreground: #8a90a0;
|
|
30
32
|
--color-placeholder-foreground: #4b4e58;
|
package/src/themes/manta.css
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
--color-card-alpha-0: rgb(255 255 255 / 0);
|
|
27
27
|
|
|
28
28
|
--color-default-foreground: #0b1b45;
|
|
29
|
+
--color-inverse: #0b1530;
|
|
30
|
+
--color-inverse-foreground: #ffffff;
|
|
29
31
|
--color-body-foreground: rgba(23, 23, 23, 0.85);
|
|
30
32
|
--color-muted-foreground: #5a6480;
|
|
31
33
|
--color-placeholder-foreground: var(--color-muted-foreground);
|
package/src/themes/standard.css
CHANGED
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
--color-rank-3: #ffe4d6;
|
|
49
49
|
|
|
50
50
|
--color-default-foreground: var(--color-neutral-900);
|
|
51
|
+
--color-inverse: var(--color-neutral-900);
|
|
52
|
+
--color-inverse-foreground: #ffffff;
|
|
51
53
|
--color-muted-foreground: var(--color-neutral-500);
|
|
52
54
|
--color-disabled-foreground: var(--color-neutral-400);
|
|
53
55
|
--color-placeholder-foreground: var(--color-neutral-400);
|