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