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