@jytextiles/medusa-plugin-faire-store-sync 0.2.0 → 0.2.2

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,15 +228,15 @@ 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;
235
- const navigate = reactRouterDom.useNavigate();
235
+ const navigate2 = reactRouterDom.useNavigate();
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({
@@ -300,7 +300,7 @@ const FaireSettingsPage = () => {
300
300
  rowCount: ((_b = syncsQuery.data) == null ? void 0 : _b.count) ?? 0,
301
301
  getRowId: (row) => row.id,
302
302
  onRowClick: (_, row) => {
303
- navigate(`/settings/faire/${row.id}`);
303
+ navigate2(`/settings/faire/${row.id}`);
304
304
  },
305
305
  isLoading: syncsQuery.isLoading,
306
306
  filters,
@@ -343,7 +343,7 @@ const FaireSettingsPage = () => {
343
343
  {
344
344
  size: "small",
345
345
  variant: "secondary",
346
- onClick: () => navigate("/settings/faire/settings"),
346
+ onClick: () => navigate2("/settings/faire/settings"),
347
347
  children: "Sync settings"
348
348
  }
349
349
  ),
@@ -485,7 +485,17 @@ const BulkOperations = ({ connected }) => {
485
485
  disabled: !connected || pushMutation.isPending
486
486
  }
487
487
  ),
488
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
488
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
489
+ /* @__PURE__ */ jsxRuntime.jsx(
490
+ ui.Button,
491
+ {
492
+ size: "small",
493
+ variant: "secondary",
494
+ disabled: !connected,
495
+ onClick: () => navigate("/settings/faire/bulk"),
496
+ children: "Select products"
497
+ }
498
+ ),
489
499
  /* @__PURE__ */ jsxRuntime.jsx(
490
500
  ui.Button,
491
501
  {
@@ -557,9 +567,256 @@ const config = adminSdk.defineRouteConfig({
557
567
  label: "Faire",
558
568
  icon: icons.BuildingStorefront
559
569
  });
560
- const handle$2 = {
570
+ const handle$3 = {
561
571
  breadcrumb: () => "Faire Sync"
562
572
  };
573
+ const Root = ({ prev = "..", children }) => {
574
+ const navigate2 = reactRouterDom.useNavigate();
575
+ const [open, setOpen] = react.useState(false);
576
+ react.useEffect(() => {
577
+ setOpen(true);
578
+ return () => setOpen(false);
579
+ }, []);
580
+ const handleOpenChange = (next) => {
581
+ if (!next) {
582
+ document.body.style.pointerEvents = "auto";
583
+ navigate2(prev, { replace: true });
584
+ return;
585
+ }
586
+ setOpen(next);
587
+ };
588
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Content, { className: "flex flex-col", children }) });
589
+ };
590
+ const RouteFocusModal = Object.assign(Root, {
591
+ Header: ui.FocusModal.Header,
592
+ Title: ui.FocusModal.Title,
593
+ Description: ui.FocusModal.Description,
594
+ Body: ui.FocusModal.Body,
595
+ Footer: ui.FocusModal.Footer,
596
+ Close: ui.FocusModal.Close
597
+ });
598
+ const columnHelper = ui.createDataTableColumnHelper();
599
+ const useBulkProductColumns = () => {
600
+ return [
601
+ columnHelper.accessor("title", {
602
+ header: "Product",
603
+ cell: ({ row, getValue }) => {
604
+ const p = row.original;
605
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
606
+ p.thumbnail ? /* @__PURE__ */ jsxRuntime.jsx(
607
+ "img",
608
+ {
609
+ src: p.thumbnail,
610
+ alt: "",
611
+ className: "h-8 w-8 rounded border border-ui-border-base object-cover"
612
+ }
613
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 w-8 rounded border border-ui-border-base bg-ui-bg-subtle" }),
614
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
615
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: getValue() }),
616
+ p.handle && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-subtle text-xs font-mono", children: p.handle })
617
+ ] })
618
+ ] });
619
+ }
620
+ }),
621
+ columnHelper.accessor("status", {
622
+ header: "Status",
623
+ cell: (info) => {
624
+ const s = info.getValue();
625
+ const color = s === "published" ? "green" : s === "draft" ? "orange" : "grey";
626
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color, children: s });
627
+ }
628
+ }),
629
+ columnHelper.accessor("updated_at", {
630
+ header: "Updated",
631
+ cell: (info) => {
632
+ const v = info.getValue();
633
+ return v ? new Date(v).toLocaleString() : "—";
634
+ }
635
+ })
636
+ ];
637
+ };
638
+ const PAGE_SIZE = 20;
639
+ const PRODUCT_FIELDS = "id,title,status,thumbnail,handle,created_at,updated_at";
640
+ const SELECT_ALL_CAP = 2e3;
641
+ const listProducts = (query) => sdk.admin.product.list({
642
+ fields: PRODUCT_FIELDS,
643
+ ...query
644
+ });
645
+ const FaireBulkPage = () => {
646
+ var _a, _b, _c, _d;
647
+ const queryClient = reactQuery.useQueryClient();
648
+ const [pagination, setPagination] = react.useState({
649
+ pageIndex: 0,
650
+ pageSize: PAGE_SIZE
651
+ });
652
+ const [filtering, setFiltering] = react.useState({});
653
+ const [search, setSearch] = react.useState("");
654
+ const [rowSelection, setRowSelection] = react.useState({});
655
+ const [selectAllMatching, setSelectAllMatching] = react.useState(false);
656
+ const statusFilter = (_a = filtering.status) == null ? void 0 : _a[0];
657
+ const statusQuery = reactQuery.useQuery({
658
+ queryKey: ["faire", "status"],
659
+ queryFn: () => faireApi.status()
660
+ });
661
+ const connected = !!((_b = statusQuery.data) == null ? void 0 : _b.connected);
662
+ const productsQuery = reactQuery.useQuery({
663
+ queryKey: ["faire", "bulk-products", pagination, statusFilter, search],
664
+ placeholderData: reactQuery.keepPreviousData,
665
+ queryFn: () => listProducts({
666
+ limit: pagination.pageSize,
667
+ offset: pagination.pageIndex * pagination.pageSize,
668
+ q: search || void 0,
669
+ status: statusFilter,
670
+ order: "-created_at"
671
+ })
672
+ });
673
+ const products = ((_c = productsQuery.data) == null ? void 0 : _c.products) ?? [];
674
+ const count = ((_d = productsQuery.data) == null ? void 0 : _d.count) ?? 0;
675
+ const selectedIds = react.useMemo(
676
+ () => Object.keys(rowSelection).filter((id) => rowSelection[id]),
677
+ [rowSelection]
678
+ );
679
+ const targetCount = selectAllMatching ? count : selectedIds.length;
680
+ const resolveTargetIds = async () => {
681
+ if (!selectAllMatching) return selectedIds;
682
+ const ids = [];
683
+ let offset = 0;
684
+ while (ids.length < count && ids.length < SELECT_ALL_CAP) {
685
+ const res = await listProducts({
686
+ limit: 200,
687
+ offset,
688
+ q: search || void 0,
689
+ status: statusFilter,
690
+ order: "-created_at"
691
+ });
692
+ const batch = res.products ?? [];
693
+ if (!batch.length) break;
694
+ ids.push(...batch.map((p) => p.id));
695
+ offset += batch.length;
696
+ }
697
+ return ids.slice(0, SELECT_ALL_CAP);
698
+ };
699
+ const pushMutation = reactQuery.useMutation({
700
+ mutationFn: async () => {
701
+ const ids = await resolveTargetIds();
702
+ if (!ids.length) throw new Error("No products to sync");
703
+ const res = await faireApi.syncBulk(ids);
704
+ return { res, queued: ids.length };
705
+ },
706
+ onSuccess: ({ res, queued }) => {
707
+ ui.toast.success("Bulk product sync started", {
708
+ description: `Queued ${queued} product(s). Batch ${res.batch_id}.` + (selectAllMatching && count > SELECT_ALL_CAP ? ` Capped at ${SELECT_ALL_CAP} — run again for the rest.` : "")
709
+ });
710
+ queryClient.invalidateQueries({ queryKey: ["faire", "syncs"] });
711
+ setRowSelection({});
712
+ setSelectAllMatching(false);
713
+ },
714
+ onError: (err) => ui.toast.error("Failed to start bulk sync", { description: err.message })
715
+ });
716
+ const columns = useBulkProductColumns();
717
+ const commands = react.useMemo(
718
+ () => [
719
+ {
720
+ label: connected ? "Push to Faire" : "Connect Faire first",
721
+ shortcut: "p",
722
+ action: () => {
723
+ if (!connected) {
724
+ ui.toast.error("Faire is not connected");
725
+ return;
726
+ }
727
+ pushMutation.mutate();
728
+ }
729
+ },
730
+ {
731
+ label: "Clear selection",
732
+ shortcut: "c",
733
+ action: () => {
734
+ setRowSelection({});
735
+ setSelectAllMatching(false);
736
+ }
737
+ }
738
+ ],
739
+ [connected, pushMutation]
740
+ );
741
+ const table = ui.useDataTable({
742
+ data: products,
743
+ columns,
744
+ rowCount: count,
745
+ getRowId: (row) => row.id,
746
+ isLoading: productsQuery.isLoading,
747
+ commands,
748
+ rowSelection: {
749
+ state: rowSelection,
750
+ onRowSelectionChange: (updater) => {
751
+ setSelectAllMatching(false);
752
+ setRowSelection(updater);
753
+ }
754
+ },
755
+ pagination: { state: pagination, onPaginationChange: setPagination },
756
+ filtering: { state: filtering, onFilteringChange: setFiltering },
757
+ search: { state: search, onSearchChange: setSearch }
758
+ });
759
+ return /* @__PURE__ */ jsxRuntime.jsxs(RouteFocusModal, { prev: "/settings/faire", children: [
760
+ /* @__PURE__ */ jsxRuntime.jsx(RouteFocusModal.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
761
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Bulk sync products to Faire" }),
762
+ /* @__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." })
763
+ ] }) }),
764
+ /* @__PURE__ */ jsxRuntime.jsx(RouteFocusModal.Body, { className: "flex flex-1 flex-col overflow-hidden p-0", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.DataTable, { instance: table, children: [
765
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.DataTable.Toolbar, { className: "flex flex-col gap-y-3 border-b px-6 py-4", children: [
766
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-y-2 md:flex-row md:items-center md:justify-between", children: [
767
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
768
+ /* @__PURE__ */ jsxRuntime.jsx(
769
+ ui.Input,
770
+ {
771
+ type: "search",
772
+ placeholder: "Search products…",
773
+ value: search,
774
+ onChange: (e) => setSearch(e.target.value),
775
+ className: "w-full md:w-72"
776
+ }
777
+ ),
778
+ !connected && /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { color: "orange", size: "2xsmall", children: "Not connected" })
779
+ ] }),
780
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
781
+ /* @__PURE__ */ jsxRuntime.jsx(
782
+ ui.Button,
783
+ {
784
+ size: "small",
785
+ variant: "secondary",
786
+ onClick: () => productsQuery.refetch(),
787
+ isLoading: productsQuery.isFetching,
788
+ children: "Refresh"
789
+ }
790
+ ),
791
+ /* @__PURE__ */ jsxRuntime.jsx(
792
+ ui.Button,
793
+ {
794
+ size: "small",
795
+ variant: selectAllMatching ? "primary" : "secondary",
796
+ disabled: !count,
797
+ onClick: () => {
798
+ setRowSelection({});
799
+ setSelectAllMatching((v) => !v);
800
+ },
801
+ children: selectAllMatching ? `All ${count} selected` : `Select all ${count}`
802
+ }
803
+ )
804
+ ] })
805
+ ] }),
806
+ targetCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: "small", className: "text-ui-fg-subtle", children: [
807
+ selectAllMatching ? `All ${count} matching product(s) will be synced` : `${targetCount} product(s) selected`,
808
+ pushMutation.isPending ? " — queuing…" : ""
809
+ ] })
810
+ ] }),
811
+ 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, {}),
812
+ /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.Pagination, {})
813
+ ] }) }),
814
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {})
815
+ ] });
816
+ };
817
+ const handle$2 = {
818
+ breadcrumb: () => "Bulk sync"
819
+ };
563
820
  const STATUS_COLORS = {
564
821
  success: "green",
565
822
  failed: "red",
@@ -569,7 +826,7 @@ const STATUS_COLORS = {
569
826
  };
570
827
  const FaireSyncDetailPage = () => {
571
828
  const { id } = reactRouterDom.useParams();
572
- const navigate = reactRouterDom.useNavigate();
829
+ const navigate2 = reactRouterDom.useNavigate();
573
830
  const [record, setRecord] = react.useState(null);
574
831
  const [loading, setLoading] = react.useState(true);
575
832
  const [retrying, setRetrying] = react.useState(false);
@@ -601,7 +858,7 @@ const FaireSyncDetailPage = () => {
601
858
  if (!record) {
602
859
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6 flex flex-col gap-4", children: [
603
860
  /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Sync record not found" }),
604
- /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => navigate("/settings/faire"), children: "Back to Faire settings" })
861
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => navigate2("/settings/faire"), children: "Back to Faire settings" })
605
862
  ] });
606
863
  }
607
864
  const warnings = record.error_message ? record.error_message.split(" | ") : [];
@@ -699,13 +956,13 @@ const cleanSettings = (form) => {
699
956
  return out;
700
957
  };
701
958
  const FaireSyncSettingsDrawer = () => {
702
- const navigate = reactRouterDom.useNavigate();
959
+ const navigate2 = reactRouterDom.useNavigate();
703
960
  const queryClient = reactQuery.useQueryClient();
704
961
  const [open, setOpen] = react.useState(true);
705
962
  const [form, setForm] = react.useState({});
706
963
  const close = () => {
707
964
  setOpen(false);
708
- navigate(PARENT);
965
+ navigate2(PARENT);
709
966
  };
710
967
  const statusQuery = reactQuery.useQuery({
711
968
  queryKey: STATUS_KEY,
@@ -864,42 +1121,67 @@ const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
864
1121
  const handle = {
865
1122
  breadcrumb: () => "Sync settings"
866
1123
  };
1124
+ const EXCHANGED_PREFIX = "faire:oauth:exchanged:";
867
1125
  const FaireOauthCallback = () => {
868
1126
  const [params] = reactRouterDom.useSearchParams();
869
- const navigate = reactRouterDom.useNavigate();
1127
+ const navigate2 = reactRouterDom.useNavigate();
870
1128
  const [error, setError] = react.useState(null);
871
- const exchangedRef = react.useRef(false);
1129
+ const [exchanging, setExchanging] = react.useState(true);
872
1130
  react.useEffect(() => {
873
- if (exchangedRef.current) return;
874
- exchangedRef.current = true;
875
- const code = params.get("code");
876
- const state = params.get("state");
877
1131
  const errParam = params.get("error");
878
1132
  if (errParam) {
879
1133
  setError(
880
1134
  `Faire authorization failed: ${params.get("error_description") || errParam}`
881
1135
  );
1136
+ setExchanging(false);
882
1137
  return;
883
1138
  }
1139
+ const code = params.get("authorization_code") || params.get("code");
1140
+ const state = params.get("state");
884
1141
  if (!code || !state) {
885
1142
  setError("Missing code or state in Faire callback.");
1143
+ setExchanging(false);
1144
+ return;
1145
+ }
1146
+ if (sessionStorage.getItem(EXCHANGED_PREFIX + code)) {
1147
+ navigate2("/settings/faire", { replace: true });
886
1148
  return;
887
1149
  }
888
- faireApi.callback(code, state).then(() => {
889
- navigate("/settings/faire", { replace: true });
890
- }).catch((err) => {
891
- setError(err.message || "Failed to complete Faire authorization.");
892
- });
893
- }, [params, navigate]);
1150
+ sessionStorage.setItem(EXCHANGED_PREFIX + code, "1");
1151
+ let cancelled = false;
1152
+ const run = async () => {
1153
+ try {
1154
+ await faireApi.callback(code, state);
1155
+ if (!cancelled) navigate2("/settings/faire", { replace: true });
1156
+ } catch (err) {
1157
+ if (!cancelled) {
1158
+ try {
1159
+ const status = await faireApi.status();
1160
+ if (status == null ? void 0 : status.connected) {
1161
+ navigate2("/settings/faire", { replace: true });
1162
+ return;
1163
+ }
1164
+ } catch {
1165
+ }
1166
+ setError(err.message || "Failed to complete Faire authorization.");
1167
+ setExchanging(false);
1168
+ }
1169
+ }
1170
+ };
1171
+ run();
1172
+ return () => {
1173
+ cancelled = true;
1174
+ };
1175
+ }, [params, navigate2]);
894
1176
  return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col items-center gap-4 p-8", children: [
895
1177
  /* @__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…" }),
1178
+ 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
1179
  error && /* @__PURE__ */ jsxRuntime.jsx(
898
1180
  ui.Button,
899
1181
  {
900
1182
  size: "small",
901
1183
  variant: "secondary",
902
- onClick: () => navigate("/settings/faire", { replace: true }),
1184
+ onClick: () => navigate2("/settings/faire", { replace: true }),
903
1185
  children: "Back to Faire settings"
904
1186
  }
905
1187
  )
@@ -917,6 +1199,11 @@ const routeModule = {
917
1199
  {
918
1200
  Component: FaireSettingsPage,
919
1201
  path: "/settings/faire",
1202
+ handle: handle$3
1203
+ },
1204
+ {
1205
+ Component: FaireBulkPage,
1206
+ path: "/settings/faire/bulk",
920
1207
  handle: handle$2
921
1208
  },
922
1209
  {