@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.
@@ -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$2 = {
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$1 = {
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 exchangedRef = react.useRef(false);
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
- faireApi.callback(code, state).then(() => {
1054
+ if (sessionStorage.getItem(EXCHANGED_PREFIX + code)) {
889
1055
  navigate("/settings/faire", { replace: true });
890
- }).catch((err) => {
891
- setError(err.message || "Failed to complete Faire authorization.");
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$2
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
  {