@jytextiles/medusa-plugin-faire-store-sync 0.2.1 → 0.2.3
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/.medusa/server/src/admin/index.js +359 -164
- package/.medusa/server/src/admin/index.mjs +361 -166
- package/.medusa/server/src/lib/crypto.js +84 -0
- package/.medusa/server/src/lib/faire-client.js +25 -1
- package/.medusa/server/src/modules/faire-sync/service.js +51 -10
- package/README.md +21 -19
- package/package.json +1 -1
- package/src/admin/components/route-focus-modal.tsx +51 -0
- package/src/admin/routes/settings/faire/bulk/page.tsx +318 -0
- package/src/admin/routes/settings/faire/bulk/use-bulk-product-columns.tsx +60 -0
- package/src/admin/routes/settings/faire/page.tsx +7 -164
- package/src/admin/routes/settings/oauth/faire/callback/page.tsx +63 -20
- package/src/lib/crypto.ts +84 -0
- package/src/lib/faire-client.ts +24 -0
- package/src/modules/faire-sync/service.ts +57 -9
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { Container, Heading, Text, StatusBadge, Alert, Tooltip, Button, createDataTableColumnHelper, toast, createDataTableFilterHelper, useDataTable, Skeleton, DataTable, Toaster, Label, DropdownMenu, IconButton, Drawer, Switch, clx } from "@medusajs/ui";
|
|
3
|
+
import { Container, Heading, Text, StatusBadge, Alert, Tooltip, Button, createDataTableColumnHelper, toast, createDataTableFilterHelper, useDataTable, Skeleton, DataTable, Toaster, Label, DropdownMenu, IconButton, FocusModal, Input, Badge, Drawer, Switch, clx } from "@medusajs/ui";
|
|
4
4
|
import { useQueryClient, useQuery, useMutation, keepPreviousData } from "@tanstack/react-query";
|
|
5
5
|
import Medusa from "@medusajs/js-sdk";
|
|
6
6
|
import { BuildingStorefront, EllipsisHorizontal, ArrowPath, ArrowUpRightOnBox } from "@medusajs/icons";
|
|
7
|
-
import { useState, useEffect,
|
|
7
|
+
import { useState, useEffect, useMemo } from "react";
|
|
8
8
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
9
9
|
const sdk = new Medusa({
|
|
10
10
|
baseUrl: "/",
|
|
@@ -174,14 +174,14 @@ const FaireProductWidget = ({ data }) => {
|
|
|
174
174
|
defineWidgetConfig({
|
|
175
175
|
zone: "product.details.side.before"
|
|
176
176
|
});
|
|
177
|
-
const columnHelper = createDataTableColumnHelper();
|
|
177
|
+
const columnHelper$1 = createDataTableColumnHelper();
|
|
178
178
|
const useFaireSyncColumns = () => {
|
|
179
179
|
return [
|
|
180
|
-
columnHelper.accessor("product_id", {
|
|
180
|
+
columnHelper$1.accessor("product_id", {
|
|
181
181
|
header: "Product",
|
|
182
182
|
cell: (info) => /* @__PURE__ */ jsx("span", { className: "font-mono text-ui-fg-subtle text-xs", children: info.getValue() })
|
|
183
183
|
}),
|
|
184
|
-
columnHelper.accessor("status", {
|
|
184
|
+
columnHelper$1.accessor("status", {
|
|
185
185
|
header: "Status",
|
|
186
186
|
cell: (info) => {
|
|
187
187
|
const status = info.getValue();
|
|
@@ -189,11 +189,11 @@ const useFaireSyncColumns = () => {
|
|
|
189
189
|
return /* @__PURE__ */ jsx(StatusBadge, { color, children: status });
|
|
190
190
|
}
|
|
191
191
|
}),
|
|
192
|
-
columnHelper.accessor("product_state", {
|
|
192
|
+
columnHelper$1.accessor("product_state", {
|
|
193
193
|
header: "State",
|
|
194
194
|
cell: (info) => info.getValue() || "—"
|
|
195
195
|
}),
|
|
196
|
-
columnHelper.display({
|
|
196
|
+
columnHelper$1.display({
|
|
197
197
|
id: "product",
|
|
198
198
|
header: "Faire product",
|
|
199
199
|
cell: ({ row }) => {
|
|
@@ -212,7 +212,7 @@ const useFaireSyncColumns = () => {
|
|
|
212
212
|
);
|
|
213
213
|
}
|
|
214
214
|
}),
|
|
215
|
-
columnHelper.accessor("synced_at", {
|
|
215
|
+
columnHelper$1.accessor("synced_at", {
|
|
216
216
|
header: "Synced",
|
|
217
217
|
cell: (info) => {
|
|
218
218
|
const v = info.getValue();
|
|
@@ -225,7 +225,7 @@ const useFaireSyncColumns = () => {
|
|
|
225
225
|
})
|
|
226
226
|
];
|
|
227
227
|
};
|
|
228
|
-
const PAGE_SIZE = 10;
|
|
228
|
+
const PAGE_SIZE$1 = 10;
|
|
229
229
|
const STATUS_KEY$1 = ["faire", "status"];
|
|
230
230
|
const FaireSettingsPage = () => {
|
|
231
231
|
var _a, _b, _c;
|
|
@@ -233,7 +233,7 @@ const FaireSettingsPage = () => {
|
|
|
233
233
|
const queryClient = useQueryClient();
|
|
234
234
|
const [pagination, setPagination] = useState({
|
|
235
235
|
pageIndex: 0,
|
|
236
|
-
pageSize: PAGE_SIZE
|
|
236
|
+
pageSize: PAGE_SIZE$1
|
|
237
237
|
});
|
|
238
238
|
const [filtering, setFiltering] = useState({});
|
|
239
239
|
const statusQuery = useQuery({
|
|
@@ -335,6 +335,15 @@ const FaireSettingsPage = () => {
|
|
|
335
335
|
] }),
|
|
336
336
|
connected ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
337
337
|
/* @__PURE__ */ jsx(StatusBadge, { color: "green", children: "Connected" }),
|
|
338
|
+
/* @__PURE__ */ jsx(
|
|
339
|
+
Button,
|
|
340
|
+
{
|
|
341
|
+
size: "small",
|
|
342
|
+
variant: "secondary",
|
|
343
|
+
onClick: () => navigate("/settings/faire/bulk"),
|
|
344
|
+
children: "Bulk sync"
|
|
345
|
+
}
|
|
346
|
+
),
|
|
338
347
|
/* @__PURE__ */ jsx(
|
|
339
348
|
Button,
|
|
340
349
|
{
|
|
@@ -377,7 +386,6 @@ const FaireSettingsPage = () => {
|
|
|
377
386
|
)
|
|
378
387
|
] })
|
|
379
388
|
] }),
|
|
380
|
-
/* @__PURE__ */ jsx(BulkOperations, { connected }),
|
|
381
389
|
/* @__PURE__ */ jsx(Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
|
|
382
390
|
/* @__PURE__ */ jsxs(DataTable.Toolbar, { className: "flex flex-col md:flex-row justify-between gap-y-4 px-6 py-4", children: [
|
|
383
391
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -408,153 +416,11 @@ const InfoField = ({ label, value }) => /* @__PURE__ */ jsxs("div", { className:
|
|
|
408
416
|
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
409
417
|
/* @__PURE__ */ jsx(Text, { children: value })
|
|
410
418
|
] });
|
|
411
|
-
const BulkOperations = ({ connected }) => {
|
|
412
|
-
var _a, _b, _c, _d, _e, _f;
|
|
413
|
-
const [productIds, setProductIds] = useState("");
|
|
414
|
-
const [pushBatch, setPushBatch] = useState(null);
|
|
415
|
-
const [pullBatch, setPullBatch] = useState(null);
|
|
416
|
-
const pushMutation = useMutation({
|
|
417
|
-
mutationFn: (ids) => faireApi.syncBulk(ids),
|
|
418
|
-
onSuccess: (res) => {
|
|
419
|
-
setPushBatch(res.batch_id);
|
|
420
|
-
toast.success("Bulk product sync started", {
|
|
421
|
-
description: "Running in the background. Polling progress…"
|
|
422
|
-
});
|
|
423
|
-
},
|
|
424
|
-
onError: (err) => toast.error("Failed to start bulk sync", { description: err.message })
|
|
425
|
-
});
|
|
426
|
-
const pullMutation = useMutation({
|
|
427
|
-
mutationFn: () => faireApi.ingestOrders(),
|
|
428
|
-
onSuccess: (res) => {
|
|
429
|
-
setPullBatch(res.batch_id);
|
|
430
|
-
toast.success("Faire order pull started", {
|
|
431
|
-
description: "Running in the background. Polling progress…"
|
|
432
|
-
});
|
|
433
|
-
},
|
|
434
|
-
onError: (err) => toast.error("Failed to start order pull", { description: err.message })
|
|
435
|
-
});
|
|
436
|
-
const pushStatus = useQuery({
|
|
437
|
-
queryKey: ["faire", "bulk", pushBatch],
|
|
438
|
-
enabled: !!pushBatch,
|
|
439
|
-
refetchInterval: (q) => {
|
|
440
|
-
var _a2, _b2;
|
|
441
|
-
return ((_b2 = (_a2 = q.state.data) == null ? void 0 : _a2.progress) == null ? void 0 : _b2.finished) ? false : 3e3;
|
|
442
|
-
},
|
|
443
|
-
queryFn: () => faireApi.bulkStatus(pushBatch)
|
|
444
|
-
});
|
|
445
|
-
const pullStatus = useQuery({
|
|
446
|
-
queryKey: ["faire", "ingest", pullBatch],
|
|
447
|
-
enabled: !!pullBatch,
|
|
448
|
-
refetchInterval: (q) => {
|
|
449
|
-
var _a2, _b2;
|
|
450
|
-
return ((_b2 = (_a2 = q.state.data) == null ? void 0 : _a2.progress) == null ? void 0 : _b2.finished) ? false : 3e3;
|
|
451
|
-
},
|
|
452
|
-
queryFn: () => faireApi.ingestStatus(pullBatch)
|
|
453
|
-
});
|
|
454
|
-
const pushProgress = (_a = pushStatus.data) == null ? void 0 : _a.progress;
|
|
455
|
-
const pullProgress = (_b = pullStatus.data) == null ? void 0 : _b.progress;
|
|
456
|
-
const handlePush = () => {
|
|
457
|
-
const ids = productIds.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
458
|
-
if (!ids.length) {
|
|
459
|
-
toast.error("Enter at least one product id");
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
pushMutation.mutate(ids);
|
|
463
|
-
};
|
|
464
|
-
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
465
|
-
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4", children: [
|
|
466
|
-
/* @__PURE__ */ jsx(Heading, { children: "Bulk operations" }),
|
|
467
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Push many products to Faire, or pull wholesale orders from Faire into Medusa. Both run as long-running background workflows." })
|
|
468
|
-
] }),
|
|
469
|
-
/* @__PURE__ */ jsxs("div", { className: "px-6 py-4 flex flex-col gap-6", children: [
|
|
470
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
471
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
472
|
-
/* @__PURE__ */ jsx(Label, { children: "Push products to Faire" }),
|
|
473
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Comma- or line-separated Medusa product IDs." })
|
|
474
|
-
] }),
|
|
475
|
-
/* @__PURE__ */ jsx(
|
|
476
|
-
"textarea",
|
|
477
|
-
{
|
|
478
|
-
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm min-h-[72px]",
|
|
479
|
-
placeholder: "prod_01..., prod_02...",
|
|
480
|
-
value: productIds,
|
|
481
|
-
onChange: (e) => setProductIds(e.target.value),
|
|
482
|
-
disabled: !connected || pushMutation.isPending
|
|
483
|
-
}
|
|
484
|
-
),
|
|
485
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
486
|
-
/* @__PURE__ */ jsx(
|
|
487
|
-
Button,
|
|
488
|
-
{
|
|
489
|
-
size: "small",
|
|
490
|
-
onClick: handlePush,
|
|
491
|
-
disabled: !connected || pushMutation.isPending,
|
|
492
|
-
isLoading: pushMutation.isPending,
|
|
493
|
-
children: "Start bulk push"
|
|
494
|
-
}
|
|
495
|
-
),
|
|
496
|
-
pushBatch && pushProgress && /* @__PURE__ */ jsx(
|
|
497
|
-
BulkProgress,
|
|
498
|
-
{
|
|
499
|
-
label: "Push",
|
|
500
|
-
progress: pushProgress,
|
|
501
|
-
status: (_d = (_c = pushStatus.data) == null ? void 0 : _c.batch) == null ? void 0 : _d.status
|
|
502
|
-
}
|
|
503
|
-
)
|
|
504
|
-
] })
|
|
505
|
-
] }),
|
|
506
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
507
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
508
|
-
/* @__PURE__ */ jsx(Label, { children: "Pull Faire orders into Medusa" }),
|
|
509
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Ingests all available Faire orders as Medusa orders (idempotent)." })
|
|
510
|
-
] }),
|
|
511
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
512
|
-
/* @__PURE__ */ jsx(
|
|
513
|
-
Button,
|
|
514
|
-
{
|
|
515
|
-
size: "small",
|
|
516
|
-
variant: "secondary",
|
|
517
|
-
onClick: () => pullMutation.mutate(),
|
|
518
|
-
disabled: !connected || pullMutation.isPending,
|
|
519
|
-
isLoading: pullMutation.isPending,
|
|
520
|
-
children: "Start order pull"
|
|
521
|
-
}
|
|
522
|
-
),
|
|
523
|
-
pullBatch && pullProgress && /* @__PURE__ */ jsx(
|
|
524
|
-
BulkProgress,
|
|
525
|
-
{
|
|
526
|
-
label: "Pull",
|
|
527
|
-
progress: pullProgress,
|
|
528
|
-
status: (_f = (_e = pullStatus.data) == null ? void 0 : _e.batch) == null ? void 0 : _f.status
|
|
529
|
-
}
|
|
530
|
-
)
|
|
531
|
-
] })
|
|
532
|
-
] })
|
|
533
|
-
] })
|
|
534
|
-
] });
|
|
535
|
-
};
|
|
536
|
-
const BulkProgress = ({
|
|
537
|
-
label,
|
|
538
|
-
progress,
|
|
539
|
-
status
|
|
540
|
-
}) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
541
|
-
/* @__PURE__ */ jsxs(StatusBadge, { color: progress.finished ? status === "failed" ? "red" : "green" : "orange", children: [
|
|
542
|
-
label,
|
|
543
|
-
": ",
|
|
544
|
-
progress.done,
|
|
545
|
-
"/",
|
|
546
|
-
progress.total || "?",
|
|
547
|
-
" (",
|
|
548
|
-
progress.pct,
|
|
549
|
-
"%)"
|
|
550
|
-
] }),
|
|
551
|
-
progress.finished && /* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: status === "failed" ? "Completed with errors" : "Done" })
|
|
552
|
-
] });
|
|
553
419
|
const config = defineRouteConfig({
|
|
554
420
|
label: "Faire",
|
|
555
421
|
icon: BuildingStorefront
|
|
556
422
|
});
|
|
557
|
-
const handle$
|
|
423
|
+
const handle$3 = {
|
|
558
424
|
breadcrumb: () => "Faire Sync"
|
|
559
425
|
};
|
|
560
426
|
const STATUS_COLORS = {
|
|
@@ -672,9 +538,308 @@ const Detail = ({
|
|
|
672
538
|
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
673
539
|
/* @__PURE__ */ jsx(Text, { className: mono ? "font-mono text-xs" : "", children: value })
|
|
674
540
|
] });
|
|
675
|
-
const handle$
|
|
541
|
+
const handle$2 = {
|
|
676
542
|
breadcrumb: () => "Sync record"
|
|
677
543
|
};
|
|
544
|
+
const Root = ({ prev = "..", children }) => {
|
|
545
|
+
const navigate = useNavigate();
|
|
546
|
+
const [open, setOpen] = useState(false);
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
setOpen(true);
|
|
549
|
+
return () => setOpen(false);
|
|
550
|
+
}, []);
|
|
551
|
+
const handleOpenChange = (next) => {
|
|
552
|
+
if (!next) {
|
|
553
|
+
document.body.style.pointerEvents = "auto";
|
|
554
|
+
navigate(prev, { replace: true });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
setOpen(next);
|
|
558
|
+
};
|
|
559
|
+
return /* @__PURE__ */ jsx(FocusModal, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsx(FocusModal.Content, { className: "flex flex-col", children }) });
|
|
560
|
+
};
|
|
561
|
+
const RouteFocusModal = Object.assign(Root, {
|
|
562
|
+
Header: FocusModal.Header,
|
|
563
|
+
Title: FocusModal.Title,
|
|
564
|
+
Description: FocusModal.Description,
|
|
565
|
+
Body: FocusModal.Body,
|
|
566
|
+
Footer: FocusModal.Footer,
|
|
567
|
+
Close: FocusModal.Close
|
|
568
|
+
});
|
|
569
|
+
const columnHelper = createDataTableColumnHelper();
|
|
570
|
+
const useBulkProductColumns = () => {
|
|
571
|
+
return [
|
|
572
|
+
columnHelper.accessor("title", {
|
|
573
|
+
header: "Product",
|
|
574
|
+
cell: ({ row, getValue }) => {
|
|
575
|
+
const p = row.original;
|
|
576
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
577
|
+
p.thumbnail ? /* @__PURE__ */ jsx(
|
|
578
|
+
"img",
|
|
579
|
+
{
|
|
580
|
+
src: p.thumbnail,
|
|
581
|
+
alt: "",
|
|
582
|
+
className: "h-8 w-8 rounded border border-ui-border-base object-cover"
|
|
583
|
+
}
|
|
584
|
+
) : /* @__PURE__ */ jsx("div", { className: "h-8 w-8 rounded border border-ui-border-base bg-ui-bg-subtle" }),
|
|
585
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
586
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: getValue() }),
|
|
587
|
+
p.handle && /* @__PURE__ */ jsx("span", { className: "text-ui-fg-subtle text-xs font-mono", children: p.handle })
|
|
588
|
+
] })
|
|
589
|
+
] });
|
|
590
|
+
}
|
|
591
|
+
}),
|
|
592
|
+
columnHelper.accessor("status", {
|
|
593
|
+
header: "Status",
|
|
594
|
+
cell: (info) => {
|
|
595
|
+
const s = info.getValue();
|
|
596
|
+
const color = s === "published" ? "green" : s === "draft" ? "orange" : "grey";
|
|
597
|
+
return /* @__PURE__ */ jsx(StatusBadge, { color, children: s });
|
|
598
|
+
}
|
|
599
|
+
}),
|
|
600
|
+
columnHelper.accessor("updated_at", {
|
|
601
|
+
header: "Updated",
|
|
602
|
+
cell: (info) => {
|
|
603
|
+
const v = info.getValue();
|
|
604
|
+
return v ? new Date(v).toLocaleString() : "—";
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
];
|
|
608
|
+
};
|
|
609
|
+
const PAGE_SIZE = 20;
|
|
610
|
+
const PRODUCT_FIELDS = "id,title,status,thumbnail,handle,created_at,updated_at";
|
|
611
|
+
const SELECT_ALL_CAP = 2e3;
|
|
612
|
+
const listProducts = (query) => sdk.admin.product.list({
|
|
613
|
+
fields: PRODUCT_FIELDS,
|
|
614
|
+
...query
|
|
615
|
+
});
|
|
616
|
+
const FaireBulkPage = () => {
|
|
617
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
618
|
+
const queryClient = useQueryClient();
|
|
619
|
+
const [pagination, setPagination] = useState({
|
|
620
|
+
pageIndex: 0,
|
|
621
|
+
pageSize: PAGE_SIZE
|
|
622
|
+
});
|
|
623
|
+
const [filtering, setFiltering] = useState({});
|
|
624
|
+
const [search, setSearch] = useState("");
|
|
625
|
+
const [rowSelection, setRowSelection] = useState({});
|
|
626
|
+
const [selectAllMatching, setSelectAllMatching] = useState(false);
|
|
627
|
+
const statusFilter = (_a = filtering.status) == null ? void 0 : _a[0];
|
|
628
|
+
const statusQuery = useQuery({
|
|
629
|
+
queryKey: ["faire", "status"],
|
|
630
|
+
queryFn: () => faireApi.status()
|
|
631
|
+
});
|
|
632
|
+
const connected = !!((_b = statusQuery.data) == null ? void 0 : _b.connected);
|
|
633
|
+
const productsQuery = useQuery({
|
|
634
|
+
queryKey: ["faire", "bulk-products", pagination, statusFilter, search],
|
|
635
|
+
placeholderData: keepPreviousData,
|
|
636
|
+
queryFn: () => listProducts({
|
|
637
|
+
limit: pagination.pageSize,
|
|
638
|
+
offset: pagination.pageIndex * pagination.pageSize,
|
|
639
|
+
q: search || void 0,
|
|
640
|
+
status: statusFilter,
|
|
641
|
+
order: "-created_at"
|
|
642
|
+
})
|
|
643
|
+
});
|
|
644
|
+
const products = ((_c = productsQuery.data) == null ? void 0 : _c.products) ?? [];
|
|
645
|
+
const count = ((_d = productsQuery.data) == null ? void 0 : _d.count) ?? 0;
|
|
646
|
+
const selectedIds = useMemo(
|
|
647
|
+
() => Object.keys(rowSelection).filter((id) => rowSelection[id]),
|
|
648
|
+
[rowSelection]
|
|
649
|
+
);
|
|
650
|
+
const targetCount = selectAllMatching ? count : selectedIds.length;
|
|
651
|
+
const resolveTargetIds = async () => {
|
|
652
|
+
if (!selectAllMatching) return selectedIds;
|
|
653
|
+
const ids = [];
|
|
654
|
+
let offset = 0;
|
|
655
|
+
while (ids.length < count && ids.length < SELECT_ALL_CAP) {
|
|
656
|
+
const res = await listProducts({
|
|
657
|
+
limit: 200,
|
|
658
|
+
offset,
|
|
659
|
+
q: search || void 0,
|
|
660
|
+
status: statusFilter,
|
|
661
|
+
order: "-created_at"
|
|
662
|
+
});
|
|
663
|
+
const batch = res.products ?? [];
|
|
664
|
+
if (!batch.length) break;
|
|
665
|
+
ids.push(...batch.map((p) => p.id));
|
|
666
|
+
offset += batch.length;
|
|
667
|
+
}
|
|
668
|
+
return ids.slice(0, SELECT_ALL_CAP);
|
|
669
|
+
};
|
|
670
|
+
const pushMutation = useMutation({
|
|
671
|
+
mutationFn: async () => {
|
|
672
|
+
const ids = await resolveTargetIds();
|
|
673
|
+
if (!ids.length) throw new Error("No products to sync");
|
|
674
|
+
const res = await faireApi.syncBulk(ids);
|
|
675
|
+
return { res, queued: ids.length };
|
|
676
|
+
},
|
|
677
|
+
onSuccess: ({ res, queued }) => {
|
|
678
|
+
toast.success("Bulk product sync started", {
|
|
679
|
+
description: `Queued ${queued} product(s). Batch ${res.batch_id}.` + (selectAllMatching && count > SELECT_ALL_CAP ? ` Capped at ${SELECT_ALL_CAP} — run again for the rest.` : "")
|
|
680
|
+
});
|
|
681
|
+
queryClient.invalidateQueries({ queryKey: ["faire", "syncs"] });
|
|
682
|
+
setRowSelection({});
|
|
683
|
+
setSelectAllMatching(false);
|
|
684
|
+
},
|
|
685
|
+
onError: (err) => toast.error("Failed to start bulk sync", { description: err.message })
|
|
686
|
+
});
|
|
687
|
+
const [pullBatch, setPullBatch] = useState(null);
|
|
688
|
+
const pullMutation = useMutation({
|
|
689
|
+
mutationFn: () => faireApi.ingestOrders(),
|
|
690
|
+
onSuccess: (res) => {
|
|
691
|
+
setPullBatch(res.batch_id);
|
|
692
|
+
toast.success("Faire order pull started", {
|
|
693
|
+
description: "Running in the background. Polling progress…"
|
|
694
|
+
});
|
|
695
|
+
},
|
|
696
|
+
onError: (err) => toast.error("Failed to start order pull", { description: err.message })
|
|
697
|
+
});
|
|
698
|
+
const pullStatus = useQuery({
|
|
699
|
+
queryKey: ["faire", "ingest", pullBatch],
|
|
700
|
+
enabled: !!pullBatch,
|
|
701
|
+
refetchInterval: (q) => {
|
|
702
|
+
var _a2, _b2;
|
|
703
|
+
return ((_b2 = (_a2 = q.state.data) == null ? void 0 : _a2.progress) == null ? void 0 : _b2.finished) ? false : 3e3;
|
|
704
|
+
},
|
|
705
|
+
queryFn: () => faireApi.ingestStatus(pullBatch)
|
|
706
|
+
});
|
|
707
|
+
const pullProgress = (_e = pullStatus.data) == null ? void 0 : _e.progress;
|
|
708
|
+
const columns = useBulkProductColumns();
|
|
709
|
+
const commands = useMemo(
|
|
710
|
+
() => [
|
|
711
|
+
{
|
|
712
|
+
label: connected ? "Push to Faire" : "Connect Faire first",
|
|
713
|
+
shortcut: "p",
|
|
714
|
+
action: () => {
|
|
715
|
+
if (!connected) {
|
|
716
|
+
toast.error("Faire is not connected");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
pushMutation.mutate();
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
label: "Clear selection",
|
|
724
|
+
shortcut: "c",
|
|
725
|
+
action: () => {
|
|
726
|
+
setRowSelection({});
|
|
727
|
+
setSelectAllMatching(false);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
],
|
|
731
|
+
[connected, pushMutation]
|
|
732
|
+
);
|
|
733
|
+
const table = useDataTable({
|
|
734
|
+
data: products,
|
|
735
|
+
columns,
|
|
736
|
+
rowCount: count,
|
|
737
|
+
getRowId: (row) => row.id,
|
|
738
|
+
isLoading: productsQuery.isLoading,
|
|
739
|
+
commands,
|
|
740
|
+
rowSelection: {
|
|
741
|
+
state: rowSelection,
|
|
742
|
+
onRowSelectionChange: (updater) => {
|
|
743
|
+
setSelectAllMatching(false);
|
|
744
|
+
setRowSelection(updater);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
pagination: { state: pagination, onPaginationChange: setPagination },
|
|
748
|
+
filtering: { state: filtering, onFilteringChange: setFiltering },
|
|
749
|
+
search: { state: search, onSearchChange: setSearch }
|
|
750
|
+
});
|
|
751
|
+
return /* @__PURE__ */ jsxs(RouteFocusModal, { prev: "/settings/faire", children: [
|
|
752
|
+
/* @__PURE__ */ jsx(RouteFocusModal.Header, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
753
|
+
/* @__PURE__ */ jsx(Heading, { children: "Bulk sync products to Faire" }),
|
|
754
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Select products (or all matching) and push them to Faire as a background sync via the command bar." })
|
|
755
|
+
] }) }),
|
|
756
|
+
/* @__PURE__ */ jsx(RouteFocusModal.Body, { className: "flex flex-1 flex-col overflow-hidden p-0", children: /* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
|
|
757
|
+
/* @__PURE__ */ jsxs(DataTable.Toolbar, { className: "flex flex-col gap-y-3 border-b px-6 py-4", children: [
|
|
758
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2 md:flex-row md:items-center md:justify-between", children: [
|
|
759
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
760
|
+
/* @__PURE__ */ jsx(
|
|
761
|
+
Input,
|
|
762
|
+
{
|
|
763
|
+
type: "search",
|
|
764
|
+
placeholder: "Search products…",
|
|
765
|
+
value: search,
|
|
766
|
+
onChange: (e) => setSearch(e.target.value),
|
|
767
|
+
className: "w-full md:w-72"
|
|
768
|
+
}
|
|
769
|
+
),
|
|
770
|
+
!connected && /* @__PURE__ */ jsx(Badge, { color: "orange", size: "2xsmall", children: "Not connected" })
|
|
771
|
+
] }),
|
|
772
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
773
|
+
/* @__PURE__ */ jsx(
|
|
774
|
+
Button,
|
|
775
|
+
{
|
|
776
|
+
size: "small",
|
|
777
|
+
variant: "secondary",
|
|
778
|
+
onClick: () => productsQuery.refetch(),
|
|
779
|
+
isLoading: productsQuery.isFetching,
|
|
780
|
+
children: "Refresh"
|
|
781
|
+
}
|
|
782
|
+
),
|
|
783
|
+
/* @__PURE__ */ jsx(
|
|
784
|
+
Button,
|
|
785
|
+
{
|
|
786
|
+
size: "small",
|
|
787
|
+
variant: selectAllMatching ? "primary" : "secondary",
|
|
788
|
+
disabled: !count,
|
|
789
|
+
onClick: () => {
|
|
790
|
+
setRowSelection({});
|
|
791
|
+
setSelectAllMatching((v) => !v);
|
|
792
|
+
},
|
|
793
|
+
children: selectAllMatching ? `All ${count} selected` : `Select all ${count}`
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
] })
|
|
797
|
+
] }),
|
|
798
|
+
targetCount > 0 && /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle", children: [
|
|
799
|
+
selectAllMatching ? `All ${count} matching product(s) will be synced` : `${targetCount} product(s) selected`,
|
|
800
|
+
pushMutation.isPending ? " — queuing…" : ""
|
|
801
|
+
] })
|
|
802
|
+
] }),
|
|
803
|
+
productsQuery.isLoading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3 p-6", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }) : /* @__PURE__ */ jsx(DataTable.Table, {}),
|
|
804
|
+
/* @__PURE__ */ jsx(DataTable.Pagination, {})
|
|
805
|
+
] }) }),
|
|
806
|
+
/* @__PURE__ */ jsx(RouteFocusModal.Footer, { children: /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center justify-between gap-x-3", children: [
|
|
807
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-3", children: [
|
|
808
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: "Pull Faire orders into Medusa (idempotent)" }),
|
|
809
|
+
pullBatch && pullProgress && /* @__PURE__ */ jsxs(
|
|
810
|
+
StatusBadge,
|
|
811
|
+
{
|
|
812
|
+
color: pullProgress.finished ? ((_g = (_f = pullStatus.data) == null ? void 0 : _f.batch) == null ? void 0 : _g.status) === "failed" ? "red" : "green" : "orange",
|
|
813
|
+
children: [
|
|
814
|
+
"Pull: ",
|
|
815
|
+
pullProgress.done,
|
|
816
|
+
"/",
|
|
817
|
+
pullProgress.total || "?",
|
|
818
|
+
" (",
|
|
819
|
+
pullProgress.pct,
|
|
820
|
+
"%)"
|
|
821
|
+
]
|
|
822
|
+
}
|
|
823
|
+
)
|
|
824
|
+
] }),
|
|
825
|
+
/* @__PURE__ */ jsx(
|
|
826
|
+
Button,
|
|
827
|
+
{
|
|
828
|
+
size: "small",
|
|
829
|
+
variant: "secondary",
|
|
830
|
+
onClick: () => pullMutation.mutate(),
|
|
831
|
+
disabled: !connected || pullMutation.isPending,
|
|
832
|
+
isLoading: pullMutation.isPending,
|
|
833
|
+
children: "Pull orders"
|
|
834
|
+
}
|
|
835
|
+
)
|
|
836
|
+
] }) }),
|
|
837
|
+
/* @__PURE__ */ jsx(Toaster, {})
|
|
838
|
+
] });
|
|
839
|
+
};
|
|
840
|
+
const handle$1 = {
|
|
841
|
+
breadcrumb: () => "Bulk sync"
|
|
842
|
+
};
|
|
678
843
|
const STATUS_KEY = ["faire", "status"];
|
|
679
844
|
const PARENT = "/settings/faire";
|
|
680
845
|
const NONE = "__none__";
|
|
@@ -861,36 +1026,61 @@ const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxs(
|
|
|
861
1026
|
const handle = {
|
|
862
1027
|
breadcrumb: () => "Sync settings"
|
|
863
1028
|
};
|
|
1029
|
+
const EXCHANGED_PREFIX = "faire:oauth:exchanged:";
|
|
864
1030
|
const FaireOauthCallback = () => {
|
|
865
1031
|
const [params] = useSearchParams();
|
|
866
1032
|
const navigate = useNavigate();
|
|
867
1033
|
const [error, setError] = useState(null);
|
|
868
|
-
const
|
|
1034
|
+
const [exchanging, setExchanging] = useState(true);
|
|
869
1035
|
useEffect(() => {
|
|
870
|
-
if (exchangedRef.current) return;
|
|
871
|
-
exchangedRef.current = true;
|
|
872
|
-
const code = params.get("code");
|
|
873
|
-
const state = params.get("state");
|
|
874
1036
|
const errParam = params.get("error");
|
|
875
1037
|
if (errParam) {
|
|
876
1038
|
setError(
|
|
877
1039
|
`Faire authorization failed: ${params.get("error_description") || errParam}`
|
|
878
1040
|
);
|
|
1041
|
+
setExchanging(false);
|
|
879
1042
|
return;
|
|
880
1043
|
}
|
|
1044
|
+
const code = params.get("authorization_code") || params.get("code");
|
|
1045
|
+
const state = params.get("state");
|
|
881
1046
|
if (!code || !state) {
|
|
882
1047
|
setError("Missing code or state in Faire callback.");
|
|
1048
|
+
setExchanging(false);
|
|
883
1049
|
return;
|
|
884
1050
|
}
|
|
885
|
-
|
|
1051
|
+
if (sessionStorage.getItem(EXCHANGED_PREFIX + code)) {
|
|
886
1052
|
navigate("/settings/faire", { replace: true });
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
sessionStorage.setItem(EXCHANGED_PREFIX + code, "1");
|
|
1056
|
+
let cancelled = false;
|
|
1057
|
+
const run = async () => {
|
|
1058
|
+
try {
|
|
1059
|
+
await faireApi.callback(code, state);
|
|
1060
|
+
if (!cancelled) navigate("/settings/faire", { replace: true });
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
if (!cancelled) {
|
|
1063
|
+
try {
|
|
1064
|
+
const status = await faireApi.status();
|
|
1065
|
+
if (status == null ? void 0 : status.connected) {
|
|
1066
|
+
navigate("/settings/faire", { replace: true });
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
setError(err.message || "Failed to complete Faire authorization.");
|
|
1072
|
+
setExchanging(false);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
run();
|
|
1077
|
+
return () => {
|
|
1078
|
+
cancelled = true;
|
|
1079
|
+
};
|
|
890
1080
|
}, [params, navigate]);
|
|
891
1081
|
return /* @__PURE__ */ jsxs(Container, { className: "flex flex-col items-center gap-4 p-8", children: [
|
|
892
1082
|
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Connecting Faire…" }),
|
|
893
|
-
error ? /* @__PURE__ */ jsx(Alert, { variant: "error", children: /* @__PURE__ */ jsx(Text, { children: error }) }) : /* @__PURE__ */ jsx(Text, { children: "Completing authorization, please wait…" }),
|
|
1083
|
+
error ? /* @__PURE__ */ jsx(Alert, { variant: "error", children: /* @__PURE__ */ jsx(Text, { children: error }) }) : /* @__PURE__ */ jsx(Text, { children: exchanging ? "Completing authorization, please wait…" : "Redirecting…" }),
|
|
894
1084
|
error && /* @__PURE__ */ jsx(
|
|
895
1085
|
Button,
|
|
896
1086
|
{
|
|
@@ -914,11 +1104,16 @@ const routeModule = {
|
|
|
914
1104
|
{
|
|
915
1105
|
Component: FaireSettingsPage,
|
|
916
1106
|
path: "/settings/faire",
|
|
917
|
-
handle: handle$
|
|
1107
|
+
handle: handle$3
|
|
918
1108
|
},
|
|
919
1109
|
{
|
|
920
1110
|
Component: FaireSyncDetailPage,
|
|
921
1111
|
path: "/settings/faire/:id",
|
|
1112
|
+
handle: handle$2
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
Component: FaireBulkPage,
|
|
1116
|
+
path: "/settings/faire/bulk",
|
|
922
1117
|
handle: handle$1
|
|
923
1118
|
},
|
|
924
1119
|
{
|