@jytextiles/medusa-plugin-faire-store-sync 0.2.8 → 0.2.10
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 +281 -268
- package/.medusa/server/src/admin/index.mjs +283 -270
- package/.medusa/server/src/api/admin/faire/taxonomy/route.js +27 -4
- package/.medusa/server/src/jobs/refresh-faire-token.js +6 -1
- package/.medusa/server/src/lib/faire-client.js +8 -3
- package/.medusa/server/src/modules/faire-sync/service.js +15 -3
- package/.medusa/server/src/workflows/sync-product-to-faire.js +15 -1
- package/package.json +1 -1
- package/src/admin/lib/api.ts +11 -4
- package/src/admin/routes/settings/faire/page.tsx +20 -14
- package/src/admin/widgets/faire-product-widget.tsx +73 -39
- package/src/api/admin/faire/taxonomy/route.ts +31 -3
- package/src/jobs/refresh-faire-token.ts +5 -0
- package/src/lib/faire-client.ts +7 -2
- package/src/modules/faire-sync/service.ts +14 -2
- package/src/workflows/sync-product-to-faire.ts +16 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
-
import { toast, Container, Heading, Text, StatusBadge, Alert, Badge, Button, Tooltip, Drawer, Input, clx, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable, Skeleton, DropdownMenu, DataTable, Label, Toaster,
|
|
3
|
+
import { toast, Container, Heading, Text, StatusBadge, Alert, Badge, Button, Tooltip, Drawer, Input, clx, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable, Skeleton, DropdownMenu, DataTable, Label, Toaster, Switch, Select, FocusModal, IconButton } from "@medusajs/ui";
|
|
4
4
|
import { useQueryClient, useQuery, useMutation, keepPreviousData } from "@tanstack/react-query";
|
|
5
|
-
import { useState,
|
|
5
|
+
import { useState, useEffect, useMemo } from "react";
|
|
6
6
|
import Medusa from "@medusajs/js-sdk";
|
|
7
7
|
import { BuildingStorefront, ChevronDownMini, EllipsisHorizontal, ArrowPath, ArrowUpRightOnBox } from "@medusajs/icons";
|
|
8
8
|
import { useNavigate, Outlet, useParams, useSearchParams } from "react-router-dom";
|
|
@@ -60,9 +60,14 @@ const faireApi = {
|
|
|
60
60
|
getSync: (id) => sdk.client.fetch(`/admin/faire/syncs/${id}`),
|
|
61
61
|
retrySync: (id) => sdk.client.fetch(`/admin/faire/syncs/${id}`, { method: "POST" }),
|
|
62
62
|
brand: () => sdk.client.fetch("/admin/faire/brand"),
|
|
63
|
-
taxonomy: () =>
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
taxonomy: (opts = {}) => {
|
|
64
|
+
var _a;
|
|
65
|
+
const query = {};
|
|
66
|
+
if (opts.q) query.q = opts.q;
|
|
67
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
68
|
+
if ((_a = opts.ids) == null ? void 0 : _a.length) query.ids = opts.ids.join(",");
|
|
69
|
+
return sdk.client.fetch("/admin/faire/taxonomy", { query });
|
|
70
|
+
},
|
|
66
71
|
products: (opts = {}) => {
|
|
67
72
|
const query = {};
|
|
68
73
|
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
@@ -71,7 +76,7 @@ const faireApi = {
|
|
|
71
76
|
return sdk.client.fetch("/admin/faire/products", { query });
|
|
72
77
|
}
|
|
73
78
|
};
|
|
74
|
-
const
|
|
79
|
+
const PICKER_LIMIT = 50;
|
|
75
80
|
const badgeColor = (status) => {
|
|
76
81
|
switch (status) {
|
|
77
82
|
case "synced":
|
|
@@ -87,7 +92,7 @@ const badgeColor = (status) => {
|
|
|
87
92
|
}
|
|
88
93
|
};
|
|
89
94
|
const FaireProductWidget = ({ data }) => {
|
|
90
|
-
var _a, _b, _c, _d;
|
|
95
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
91
96
|
const queryClient = useQueryClient();
|
|
92
97
|
const productKey = ["faire", "product-status", data.id];
|
|
93
98
|
const statusQuery = useQuery({
|
|
@@ -134,38 +139,46 @@ const FaireProductWidget = ({ data }) => {
|
|
|
134
139
|
const productType = (_d = data.type) == null ? void 0 : _d.value;
|
|
135
140
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
136
141
|
const [pickerSearch, setPickerSearch] = useState("");
|
|
137
|
-
const [
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const t = setTimeout(() => setDebouncedSearch(pickerSearch.trim()), 250);
|
|
145
|
+
return () => clearTimeout(t);
|
|
146
|
+
}, [pickerSearch]);
|
|
147
|
+
const [pinnedOverride, setPinnedOverride] = useState(void 0);
|
|
140
148
|
const taxonomyQuery = useQuery({
|
|
141
|
-
queryKey: ["faire", "taxonomy"],
|
|
149
|
+
queryKey: ["faire", "taxonomy", "search", debouncedSearch],
|
|
142
150
|
enabled: pickerOpen,
|
|
143
|
-
|
|
151
|
+
placeholderData: keepPreviousData,
|
|
152
|
+
queryFn: async () => faireApi.taxonomy({
|
|
153
|
+
q: debouncedSearch || void 0,
|
|
154
|
+
limit: PICKER_LIMIT
|
|
155
|
+
})
|
|
144
156
|
});
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
const results = ((_e = taxonomyQuery.data) == null ? void 0 : _e.taxonomy) ?? [];
|
|
158
|
+
const matchCount = ((_f = taxonomyQuery.data) == null ? void 0 : _f.count) ?? 0;
|
|
159
|
+
const pinnedId = pinnedOverride !== void 0 ? pinnedOverride.id ?? void 0 : metadata.faire_taxonomy_type_id;
|
|
160
|
+
const pinnedNameFromMeta = pinnedOverride !== void 0 ? pinnedOverride.name ?? void 0 : metadata.faire_taxonomy_type_name;
|
|
161
|
+
const pinnedResolveQuery = useQuery({
|
|
162
|
+
queryKey: ["faire", "taxonomy", "resolve", pinnedId],
|
|
163
|
+
enabled: !!pinnedId && !pinnedNameFromMeta && /^tt_/.test(String(pinnedId)),
|
|
164
|
+
queryFn: async () => (await faireApi.taxonomy({ ids: [pinnedId] })).taxonomy
|
|
165
|
+
});
|
|
166
|
+
const pinnedName = pinnedNameFromMeta ?? (pinnedId && /^tt_/.test(String(pinnedId)) ? (_h = (_g = pinnedResolveQuery.data) == null ? void 0 : _g[0]) == null ? void 0 : _h.name : pinnedId ? String(pinnedId) : void 0);
|
|
153
167
|
const resolvedSource = pinnedId ? { label: pinnedName || String(pinnedId), from: "pinned" } : metadata.faire_category ? { label: String(metadata.faire_category), from: "metadata" } : productType ? { label: productType, from: "type" } : { label: "Account fallback", from: "fallback" };
|
|
154
|
-
const filteredTaxonomy = useMemo(() => {
|
|
155
|
-
const q = pickerSearch.trim().toLowerCase();
|
|
156
|
-
const list = q ? taxonomy.filter((t) => t.name.toLowerCase().includes(q)) : taxonomy;
|
|
157
|
-
return list.slice(0, MAX_RENDERED);
|
|
158
|
-
}, [taxonomy, pickerSearch]);
|
|
159
168
|
const saveCategory = useMutation({
|
|
160
|
-
mutationFn: (
|
|
161
|
-
metadata: {
|
|
169
|
+
mutationFn: (choice) => sdk.admin.product.update(data.id, {
|
|
170
|
+
metadata: {
|
|
171
|
+
...metadata,
|
|
172
|
+
faire_taxonomy_type_id: (choice == null ? void 0 : choice.id) ?? null,
|
|
173
|
+
faire_taxonomy_type_name: (choice == null ? void 0 : choice.name) ?? null
|
|
174
|
+
}
|
|
162
175
|
}),
|
|
163
|
-
onSuccess: (_res,
|
|
164
|
-
setPinnedOverride(
|
|
176
|
+
onSuccess: (_res, choice) => {
|
|
177
|
+
setPinnedOverride({ id: (choice == null ? void 0 : choice.id) ?? null, name: (choice == null ? void 0 : choice.name) ?? null });
|
|
165
178
|
queryClient.invalidateQueries({ queryKey: ["product", data.id] });
|
|
166
179
|
setPickerOpen(false);
|
|
167
180
|
setPickerSearch("");
|
|
168
|
-
toast.success(
|
|
181
|
+
toast.success(choice ? "Faire category set" : "Faire category cleared");
|
|
169
182
|
},
|
|
170
183
|
onError: (err) => toast.error("Failed to save category", { description: err == null ? void 0 : err.message })
|
|
171
184
|
});
|
|
@@ -248,15 +261,11 @@ const FaireProductWidget = ({ data }) => {
|
|
|
248
261
|
}
|
|
249
262
|
),
|
|
250
263
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 overflow-y-auto", children: [
|
|
251
|
-
taxonomyQuery.isLoading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle px-1 py-2", children: "Loading categories…" }) :
|
|
252
|
-
"No categories match “",
|
|
253
|
-
pickerSearch,
|
|
254
|
-
"”."
|
|
255
|
-
] }) : filteredTaxonomy.map((t) => /* @__PURE__ */ jsxs(
|
|
264
|
+
taxonomyQuery.isLoading ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle px-1 py-2", children: "Loading categories…" }) : results.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle px-1 py-2", children: debouncedSearch ? `No categories match “${debouncedSearch}”.` : "No categories available." }) : results.map((t) => /* @__PURE__ */ jsxs(
|
|
256
265
|
"button",
|
|
257
266
|
{
|
|
258
267
|
type: "button",
|
|
259
|
-
onClick: () => saveCategory.mutate(t
|
|
268
|
+
onClick: () => saveCategory.mutate(t),
|
|
260
269
|
disabled: saveCategory.isPending,
|
|
261
270
|
className: clx(
|
|
262
271
|
"flex items-center justify-between rounded-md px-3 py-2 text-left text-sm",
|
|
@@ -270,12 +279,12 @@ const FaireProductWidget = ({ data }) => {
|
|
|
270
279
|
},
|
|
271
280
|
t.id
|
|
272
281
|
)),
|
|
273
|
-
!taxonomyQuery.isLoading &&
|
|
282
|
+
!taxonomyQuery.isLoading && matchCount > results.length && /* @__PURE__ */ jsxs(Text, { size: "xsmall", className: "text-ui-fg-muted px-1 py-1", children: [
|
|
274
283
|
"Showing ",
|
|
275
|
-
|
|
284
|
+
results.length,
|
|
276
285
|
" of ",
|
|
277
|
-
|
|
278
|
-
" — refine your search to narrow down."
|
|
286
|
+
matchCount,
|
|
287
|
+
" matches — refine your search to narrow down."
|
|
279
288
|
] })
|
|
280
289
|
] })
|
|
281
290
|
] }),
|
|
@@ -473,24 +482,28 @@ const FaireSettingsPage = () => {
|
|
|
473
482
|
] }),
|
|
474
483
|
connected ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
475
484
|
/* @__PURE__ */ jsx(StatusBadge, { color: "green", children: "Connected" }),
|
|
476
|
-
/* @__PURE__ */
|
|
477
|
-
Button,
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
485
|
+
/* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
486
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { size: "small", variant: "secondary", children: [
|
|
487
|
+
"Sync",
|
|
488
|
+
/* @__PURE__ */ jsx(ChevronDownMini, {})
|
|
489
|
+
] }) }),
|
|
490
|
+
/* @__PURE__ */ jsxs(DropdownMenu.Content, { children: [
|
|
491
|
+
/* @__PURE__ */ jsx(
|
|
492
|
+
DropdownMenu.Item,
|
|
493
|
+
{
|
|
494
|
+
onClick: () => navigate("/settings/faire/bulk"),
|
|
495
|
+
children: "Bulk sync"
|
|
496
|
+
}
|
|
497
|
+
),
|
|
498
|
+
/* @__PURE__ */ jsx(
|
|
499
|
+
DropdownMenu.Item,
|
|
500
|
+
{
|
|
501
|
+
onClick: () => navigate("/settings/faire/settings"),
|
|
502
|
+
children: "Sync settings"
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
] })
|
|
506
|
+
] }),
|
|
494
507
|
/* @__PURE__ */ jsx(
|
|
495
508
|
Button,
|
|
496
509
|
{
|
|
@@ -603,6 +616,213 @@ const config = defineRouteConfig({
|
|
|
603
616
|
const handle$3 = {
|
|
604
617
|
breadcrumb: () => "Faire Sync"
|
|
605
618
|
};
|
|
619
|
+
const STATUS_KEY = ["faire", "status"];
|
|
620
|
+
const TAXONOMY_KEY = ["faire", "taxonomy"];
|
|
621
|
+
const PARENT$1 = "/settings/faire";
|
|
622
|
+
const NONE = "__none__";
|
|
623
|
+
const cleanSettings = (form) => {
|
|
624
|
+
const out = { ...form };
|
|
625
|
+
for (const key of [
|
|
626
|
+
"default_brand_id",
|
|
627
|
+
"default_category"
|
|
628
|
+
]) {
|
|
629
|
+
if (!out[key] || out[key] === NONE) out[key] = null;
|
|
630
|
+
}
|
|
631
|
+
if (out.default_wholesale_markup_percent === "" || out.default_wholesale_markup_percent === NONE) {
|
|
632
|
+
out.default_wholesale_markup_percent = null;
|
|
633
|
+
}
|
|
634
|
+
if (out.default_lead_time_days === "" || out.default_lead_time_days === NONE) {
|
|
635
|
+
out.default_lead_time_days = null;
|
|
636
|
+
}
|
|
637
|
+
return out;
|
|
638
|
+
};
|
|
639
|
+
const FaireSyncSettingsDrawer = () => {
|
|
640
|
+
const navigate = useNavigate();
|
|
641
|
+
const queryClient = useQueryClient();
|
|
642
|
+
const [open, setOpen] = useState(true);
|
|
643
|
+
const [form, setForm] = useState({});
|
|
644
|
+
const close = () => {
|
|
645
|
+
setOpen(false);
|
|
646
|
+
navigate(PARENT$1);
|
|
647
|
+
};
|
|
648
|
+
const statusQuery = useQuery({
|
|
649
|
+
queryKey: STATUS_KEY,
|
|
650
|
+
queryFn: () => faireApi.status()
|
|
651
|
+
});
|
|
652
|
+
const status = statusQuery.data;
|
|
653
|
+
const connected = !!(status == null ? void 0 : status.connected);
|
|
654
|
+
const taxonomyQuery = useQuery({
|
|
655
|
+
queryKey: TAXONOMY_KEY,
|
|
656
|
+
queryFn: async () => (await faireApi.taxonomy().catch(() => ({ taxonomy: [] }))).taxonomy || []
|
|
657
|
+
});
|
|
658
|
+
const taxonomy = taxonomyQuery.data ?? [];
|
|
659
|
+
useEffect(() => {
|
|
660
|
+
if (status == null ? void 0 : status.settings) setForm(status.settings);
|
|
661
|
+
}, [status == null ? void 0 : status.settings]);
|
|
662
|
+
const saveMutation = useMutation({
|
|
663
|
+
mutationFn: (payload) => faireApi.saveSettings(payload),
|
|
664
|
+
onSuccess: () => {
|
|
665
|
+
queryClient.invalidateQueries({ queryKey: STATUS_KEY });
|
|
666
|
+
toast.success("Sync settings saved");
|
|
667
|
+
close();
|
|
668
|
+
},
|
|
669
|
+
onError: (err) => toast.error("Failed to save settings", { description: err.message })
|
|
670
|
+
});
|
|
671
|
+
return /* @__PURE__ */ jsxs(Drawer, { open, onOpenChange: (o) => o ? setOpen(true) : close(), children: [
|
|
672
|
+
/* @__PURE__ */ jsxs(Drawer.Content, { children: [
|
|
673
|
+
/* @__PURE__ */ jsxs(Drawer.Header, { children: [
|
|
674
|
+
/* @__PURE__ */ jsx(Drawer.Title, { children: "Sync settings" }),
|
|
675
|
+
/* @__PURE__ */ jsx(Drawer.Description, { children: "Publish readiness and the defaults applied to every product pushed to Faire." })
|
|
676
|
+
] }),
|
|
677
|
+
/* @__PURE__ */ jsx(Drawer.Body, { className: "flex flex-col gap-6 overflow-y-auto", children: statusQuery.isLoading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
678
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
679
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
680
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Publish readiness" }),
|
|
681
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Faire needs a brand and a wholesale-pricing strategy to publish products. Missing fields sync products as drafts." })
|
|
682
|
+
] }),
|
|
683
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
684
|
+
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.connected, label: "Faire connected" }),
|
|
685
|
+
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.brand, label: "Brand configured" }),
|
|
686
|
+
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.wholesale_pricing, label: "Wholesale pricing" }),
|
|
687
|
+
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.taxonomy, label: "Default category" })
|
|
688
|
+
] })
|
|
689
|
+
] }),
|
|
690
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
691
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
692
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Sync defaults" }),
|
|
693
|
+
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Applied to every product unless overridden in product metadata." })
|
|
694
|
+
] }),
|
|
695
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4", children: [
|
|
696
|
+
/* @__PURE__ */ jsx(Field, { label: "Brand ID", children: /* @__PURE__ */ jsx(
|
|
697
|
+
Input,
|
|
698
|
+
{
|
|
699
|
+
value: String(form.default_brand_id ?? ""),
|
|
700
|
+
onChange: (e) => setForm({ ...form, default_brand_id: e.target.value || null }),
|
|
701
|
+
disabled: !connected,
|
|
702
|
+
placeholder: "b_..."
|
|
703
|
+
}
|
|
704
|
+
) }),
|
|
705
|
+
/* @__PURE__ */ jsx(Field, { label: "Wholesale markup % (off retail)", children: /* @__PURE__ */ jsx(Tooltip, { content: "Wholesale price = retail × (100 − markup%) / 100. E.g. 50 → half of retail.", children: /* @__PURE__ */ jsx(
|
|
706
|
+
Input,
|
|
707
|
+
{
|
|
708
|
+
type: "number",
|
|
709
|
+
value: String(form.default_wholesale_markup_percent ?? ""),
|
|
710
|
+
onChange: (e) => setForm({
|
|
711
|
+
...form,
|
|
712
|
+
default_wholesale_markup_percent: e.target.value ? Number(e.target.value) : null
|
|
713
|
+
}),
|
|
714
|
+
disabled: !connected,
|
|
715
|
+
placeholder: "e.g. 50"
|
|
716
|
+
}
|
|
717
|
+
) }) }),
|
|
718
|
+
/* @__PURE__ */ jsx(Field, { label: "Default min order quantity", children: /* @__PURE__ */ jsx(
|
|
719
|
+
Input,
|
|
720
|
+
{
|
|
721
|
+
type: "number",
|
|
722
|
+
value: String(form.default_min_order_quantity ?? 1),
|
|
723
|
+
onChange: (e) => setForm({
|
|
724
|
+
...form,
|
|
725
|
+
default_min_order_quantity: e.target.value ? Number(e.target.value) : 1
|
|
726
|
+
}),
|
|
727
|
+
disabled: !connected
|
|
728
|
+
}
|
|
729
|
+
) }),
|
|
730
|
+
/* @__PURE__ */ jsx(Field, { label: "Default lead time (days)", children: /* @__PURE__ */ jsx(
|
|
731
|
+
Input,
|
|
732
|
+
{
|
|
733
|
+
type: "number",
|
|
734
|
+
value: String(form.default_lead_time_days ?? ""),
|
|
735
|
+
onChange: (e) => setForm({
|
|
736
|
+
...form,
|
|
737
|
+
default_lead_time_days: e.target.value ? Number(e.target.value) : null
|
|
738
|
+
}),
|
|
739
|
+
disabled: !connected,
|
|
740
|
+
placeholder: "e.g. 14"
|
|
741
|
+
}
|
|
742
|
+
) }),
|
|
743
|
+
/* @__PURE__ */ jsx(Field, { label: "Fallback category (taxonomy)", children: /* @__PURE__ */ jsx(Tooltip, { content: "Faire requires a category per product. Resolution order: product metadata → the product's Type (matched by name) → this fallback. Set the product Type or pick a category on the product page for accuracy.", children: /* @__PURE__ */ jsx(
|
|
744
|
+
SelectField,
|
|
745
|
+
{
|
|
746
|
+
value: String(form.default_category ?? ""),
|
|
747
|
+
onValueChange: (v) => setForm({ ...form, default_category: v || null }),
|
|
748
|
+
disabled: !connected,
|
|
749
|
+
options: taxonomy.map((t) => ({ value: t.id, label: t.name }))
|
|
750
|
+
}
|
|
751
|
+
) }) }),
|
|
752
|
+
/* @__PURE__ */ jsx(Field, { label: "Follow product status", children: /* @__PURE__ */ jsx(Tooltip, { content: "If on, published Medusa products are published on Faire; draft products sync as drafts.", children: /* @__PURE__ */ jsx(
|
|
753
|
+
Switch,
|
|
754
|
+
{
|
|
755
|
+
checked: form.follow_product_status !== false,
|
|
756
|
+
onCheckedChange: (v) => setForm({ ...form, follow_product_status: v })
|
|
757
|
+
}
|
|
758
|
+
) }) }),
|
|
759
|
+
/* @__PURE__ */ jsx(Field, { label: "Auto publish", children: /* @__PURE__ */ jsx(Tooltip, { content: "Publish products to Faire automatically on sync (otherwise create as draft).", children: /* @__PURE__ */ jsx(
|
|
760
|
+
Switch,
|
|
761
|
+
{
|
|
762
|
+
checked: !!form.auto_publish,
|
|
763
|
+
onCheckedChange: (v) => setForm({ ...form, auto_publish: v })
|
|
764
|
+
}
|
|
765
|
+
) }) })
|
|
766
|
+
] })
|
|
767
|
+
] })
|
|
768
|
+
] }) }),
|
|
769
|
+
/* @__PURE__ */ jsxs(Drawer.Footer, { children: [
|
|
770
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, children: "Cancel" }),
|
|
771
|
+
/* @__PURE__ */ jsx(
|
|
772
|
+
Button,
|
|
773
|
+
{
|
|
774
|
+
onClick: () => saveMutation.mutate(cleanSettings(form)),
|
|
775
|
+
isLoading: saveMutation.isPending,
|
|
776
|
+
disabled: !connected,
|
|
777
|
+
children: "Save settings"
|
|
778
|
+
}
|
|
779
|
+
)
|
|
780
|
+
] })
|
|
781
|
+
] }),
|
|
782
|
+
/* @__PURE__ */ jsx(Toaster, {})
|
|
783
|
+
] });
|
|
784
|
+
};
|
|
785
|
+
const Field = ({ label, children }) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
786
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
787
|
+
children
|
|
788
|
+
] });
|
|
789
|
+
const SelectField = ({
|
|
790
|
+
value,
|
|
791
|
+
onValueChange,
|
|
792
|
+
options,
|
|
793
|
+
disabled
|
|
794
|
+
}) => /* @__PURE__ */ jsxs(
|
|
795
|
+
Select,
|
|
796
|
+
{
|
|
797
|
+
value: value ? value : NONE,
|
|
798
|
+
onValueChange: (v) => onValueChange(v === NONE ? "" : v),
|
|
799
|
+
disabled,
|
|
800
|
+
size: "small",
|
|
801
|
+
children: [
|
|
802
|
+
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Not set" }) }),
|
|
803
|
+
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
804
|
+
/* @__PURE__ */ jsx(Select.Item, { value: NONE, children: "Not set" }),
|
|
805
|
+
options.map((o) => /* @__PURE__ */ jsx(Select.Item, { value: o.value, children: o.label }, o.value))
|
|
806
|
+
] })
|
|
807
|
+
]
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxs(
|
|
811
|
+
"div",
|
|
812
|
+
{
|
|
813
|
+
className: clx(
|
|
814
|
+
"flex items-center gap-2 rounded-lg border px-3 py-2",
|
|
815
|
+
ok ? "border-ui-tag-green-border bg-ui-tag-green-bg" : "border-ui-tag-red-border bg-ui-tag-red-bg"
|
|
816
|
+
),
|
|
817
|
+
children: [
|
|
818
|
+
/* @__PURE__ */ jsx(StatusBadge, { color: ok ? "green" : "red", children: ok ? "Ready" : "Missing" }),
|
|
819
|
+
/* @__PURE__ */ jsx(Text, { children: label })
|
|
820
|
+
]
|
|
821
|
+
}
|
|
822
|
+
);
|
|
823
|
+
const handle$2 = {
|
|
824
|
+
breadcrumb: () => "Sync settings"
|
|
825
|
+
};
|
|
606
826
|
const Root = ({ prev = "..", children }) => {
|
|
607
827
|
const navigate = useNavigate();
|
|
608
828
|
const [open, setOpen] = useState(false);
|
|
@@ -912,215 +1132,8 @@ const FaireBulkPage = () => {
|
|
|
912
1132
|
/* @__PURE__ */ jsx(Toaster, {})
|
|
913
1133
|
] });
|
|
914
1134
|
};
|
|
915
|
-
const handle$2 = {
|
|
916
|
-
breadcrumb: () => "Bulk sync"
|
|
917
|
-
};
|
|
918
|
-
const STATUS_KEY = ["faire", "status"];
|
|
919
|
-
const TAXONOMY_KEY = ["faire", "taxonomy"];
|
|
920
|
-
const PARENT$1 = "/settings/faire";
|
|
921
|
-
const NONE = "__none__";
|
|
922
|
-
const cleanSettings = (form) => {
|
|
923
|
-
const out = { ...form };
|
|
924
|
-
for (const key of [
|
|
925
|
-
"default_brand_id",
|
|
926
|
-
"default_category"
|
|
927
|
-
]) {
|
|
928
|
-
if (!out[key] || out[key] === NONE) out[key] = null;
|
|
929
|
-
}
|
|
930
|
-
if (out.default_wholesale_markup_percent === "" || out.default_wholesale_markup_percent === NONE) {
|
|
931
|
-
out.default_wholesale_markup_percent = null;
|
|
932
|
-
}
|
|
933
|
-
if (out.default_lead_time_days === "" || out.default_lead_time_days === NONE) {
|
|
934
|
-
out.default_lead_time_days = null;
|
|
935
|
-
}
|
|
936
|
-
return out;
|
|
937
|
-
};
|
|
938
|
-
const FaireSyncSettingsDrawer = () => {
|
|
939
|
-
const navigate = useNavigate();
|
|
940
|
-
const queryClient = useQueryClient();
|
|
941
|
-
const [open, setOpen] = useState(true);
|
|
942
|
-
const [form, setForm] = useState({});
|
|
943
|
-
const close = () => {
|
|
944
|
-
setOpen(false);
|
|
945
|
-
navigate(PARENT$1);
|
|
946
|
-
};
|
|
947
|
-
const statusQuery = useQuery({
|
|
948
|
-
queryKey: STATUS_KEY,
|
|
949
|
-
queryFn: () => faireApi.status()
|
|
950
|
-
});
|
|
951
|
-
const status = statusQuery.data;
|
|
952
|
-
const connected = !!(status == null ? void 0 : status.connected);
|
|
953
|
-
const taxonomyQuery = useQuery({
|
|
954
|
-
queryKey: TAXONOMY_KEY,
|
|
955
|
-
queryFn: async () => (await faireApi.taxonomy().catch(() => ({ taxonomy: [] }))).taxonomy || []
|
|
956
|
-
});
|
|
957
|
-
const taxonomy = taxonomyQuery.data ?? [];
|
|
958
|
-
useEffect(() => {
|
|
959
|
-
if (status == null ? void 0 : status.settings) setForm(status.settings);
|
|
960
|
-
}, [status == null ? void 0 : status.settings]);
|
|
961
|
-
const saveMutation = useMutation({
|
|
962
|
-
mutationFn: (payload) => faireApi.saveSettings(payload),
|
|
963
|
-
onSuccess: () => {
|
|
964
|
-
queryClient.invalidateQueries({ queryKey: STATUS_KEY });
|
|
965
|
-
toast.success("Sync settings saved");
|
|
966
|
-
close();
|
|
967
|
-
},
|
|
968
|
-
onError: (err) => toast.error("Failed to save settings", { description: err.message })
|
|
969
|
-
});
|
|
970
|
-
return /* @__PURE__ */ jsxs(Drawer, { open, onOpenChange: (o) => o ? setOpen(true) : close(), children: [
|
|
971
|
-
/* @__PURE__ */ jsxs(Drawer.Content, { children: [
|
|
972
|
-
/* @__PURE__ */ jsxs(Drawer.Header, { children: [
|
|
973
|
-
/* @__PURE__ */ jsx(Drawer.Title, { children: "Sync settings" }),
|
|
974
|
-
/* @__PURE__ */ jsx(Drawer.Description, { children: "Publish readiness and the defaults applied to every product pushed to Faire." })
|
|
975
|
-
] }),
|
|
976
|
-
/* @__PURE__ */ jsx(Drawer.Body, { className: "flex flex-col gap-6 overflow-y-auto", children: statusQuery.isLoading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
977
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
978
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
979
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Publish readiness" }),
|
|
980
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Faire needs a brand and a wholesale-pricing strategy to publish products. Missing fields sync products as drafts." })
|
|
981
|
-
] }),
|
|
982
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
983
|
-
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.connected, label: "Faire connected" }),
|
|
984
|
-
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.brand, label: "Brand configured" }),
|
|
985
|
-
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.wholesale_pricing, label: "Wholesale pricing" }),
|
|
986
|
-
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.taxonomy, label: "Default category" })
|
|
987
|
-
] })
|
|
988
|
-
] }),
|
|
989
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
990
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
991
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Sync defaults" }),
|
|
992
|
-
/* @__PURE__ */ jsx(Text, { className: "text-ui-fg-subtle", size: "small", children: "Applied to every product unless overridden in product metadata." })
|
|
993
|
-
] }),
|
|
994
|
-
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4", children: [
|
|
995
|
-
/* @__PURE__ */ jsx(Field, { label: "Brand ID", children: /* @__PURE__ */ jsx(
|
|
996
|
-
Input,
|
|
997
|
-
{
|
|
998
|
-
value: String(form.default_brand_id ?? ""),
|
|
999
|
-
onChange: (e) => setForm({ ...form, default_brand_id: e.target.value || null }),
|
|
1000
|
-
disabled: !connected,
|
|
1001
|
-
placeholder: "b_..."
|
|
1002
|
-
}
|
|
1003
|
-
) }),
|
|
1004
|
-
/* @__PURE__ */ jsx(Field, { label: "Wholesale markup % (off retail)", children: /* @__PURE__ */ jsx(Tooltip, { content: "Wholesale price = retail × (100 − markup%) / 100. E.g. 50 → half of retail.", children: /* @__PURE__ */ jsx(
|
|
1005
|
-
Input,
|
|
1006
|
-
{
|
|
1007
|
-
type: "number",
|
|
1008
|
-
value: String(form.default_wholesale_markup_percent ?? ""),
|
|
1009
|
-
onChange: (e) => setForm({
|
|
1010
|
-
...form,
|
|
1011
|
-
default_wholesale_markup_percent: e.target.value ? Number(e.target.value) : null
|
|
1012
|
-
}),
|
|
1013
|
-
disabled: !connected,
|
|
1014
|
-
placeholder: "e.g. 50"
|
|
1015
|
-
}
|
|
1016
|
-
) }) }),
|
|
1017
|
-
/* @__PURE__ */ jsx(Field, { label: "Default min order quantity", children: /* @__PURE__ */ jsx(
|
|
1018
|
-
Input,
|
|
1019
|
-
{
|
|
1020
|
-
type: "number",
|
|
1021
|
-
value: String(form.default_min_order_quantity ?? 1),
|
|
1022
|
-
onChange: (e) => setForm({
|
|
1023
|
-
...form,
|
|
1024
|
-
default_min_order_quantity: e.target.value ? Number(e.target.value) : 1
|
|
1025
|
-
}),
|
|
1026
|
-
disabled: !connected
|
|
1027
|
-
}
|
|
1028
|
-
) }),
|
|
1029
|
-
/* @__PURE__ */ jsx(Field, { label: "Default lead time (days)", children: /* @__PURE__ */ jsx(
|
|
1030
|
-
Input,
|
|
1031
|
-
{
|
|
1032
|
-
type: "number",
|
|
1033
|
-
value: String(form.default_lead_time_days ?? ""),
|
|
1034
|
-
onChange: (e) => setForm({
|
|
1035
|
-
...form,
|
|
1036
|
-
default_lead_time_days: e.target.value ? Number(e.target.value) : null
|
|
1037
|
-
}),
|
|
1038
|
-
disabled: !connected,
|
|
1039
|
-
placeholder: "e.g. 14"
|
|
1040
|
-
}
|
|
1041
|
-
) }),
|
|
1042
|
-
/* @__PURE__ */ jsx(Field, { label: "Fallback category (taxonomy)", children: /* @__PURE__ */ jsx(Tooltip, { content: "Faire requires a category per product. Resolution order: product metadata → the product's Type (matched by name) → this fallback. Set the product Type or pick a category on the product page for accuracy.", children: /* @__PURE__ */ jsx(
|
|
1043
|
-
SelectField,
|
|
1044
|
-
{
|
|
1045
|
-
value: String(form.default_category ?? ""),
|
|
1046
|
-
onValueChange: (v) => setForm({ ...form, default_category: v || null }),
|
|
1047
|
-
disabled: !connected,
|
|
1048
|
-
options: taxonomy.map((t) => ({ value: t.id, label: t.name }))
|
|
1049
|
-
}
|
|
1050
|
-
) }) }),
|
|
1051
|
-
/* @__PURE__ */ jsx(Field, { label: "Follow product status", children: /* @__PURE__ */ jsx(Tooltip, { content: "If on, published Medusa products are published on Faire; draft products sync as drafts.", children: /* @__PURE__ */ jsx(
|
|
1052
|
-
Switch,
|
|
1053
|
-
{
|
|
1054
|
-
checked: form.follow_product_status !== false,
|
|
1055
|
-
onCheckedChange: (v) => setForm({ ...form, follow_product_status: v })
|
|
1056
|
-
}
|
|
1057
|
-
) }) }),
|
|
1058
|
-
/* @__PURE__ */ jsx(Field, { label: "Auto publish", children: /* @__PURE__ */ jsx(Tooltip, { content: "Publish products to Faire automatically on sync (otherwise create as draft).", children: /* @__PURE__ */ jsx(
|
|
1059
|
-
Switch,
|
|
1060
|
-
{
|
|
1061
|
-
checked: !!form.auto_publish,
|
|
1062
|
-
onCheckedChange: (v) => setForm({ ...form, auto_publish: v })
|
|
1063
|
-
}
|
|
1064
|
-
) }) })
|
|
1065
|
-
] })
|
|
1066
|
-
] })
|
|
1067
|
-
] }) }),
|
|
1068
|
-
/* @__PURE__ */ jsxs(Drawer.Footer, { children: [
|
|
1069
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, children: "Cancel" }),
|
|
1070
|
-
/* @__PURE__ */ jsx(
|
|
1071
|
-
Button,
|
|
1072
|
-
{
|
|
1073
|
-
onClick: () => saveMutation.mutate(cleanSettings(form)),
|
|
1074
|
-
isLoading: saveMutation.isPending,
|
|
1075
|
-
disabled: !connected,
|
|
1076
|
-
children: "Save settings"
|
|
1077
|
-
}
|
|
1078
|
-
)
|
|
1079
|
-
] })
|
|
1080
|
-
] }),
|
|
1081
|
-
/* @__PURE__ */ jsx(Toaster, {})
|
|
1082
|
-
] });
|
|
1083
|
-
};
|
|
1084
|
-
const Field = ({ label, children }) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
1085
|
-
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
1086
|
-
children
|
|
1087
|
-
] });
|
|
1088
|
-
const SelectField = ({
|
|
1089
|
-
value,
|
|
1090
|
-
onValueChange,
|
|
1091
|
-
options,
|
|
1092
|
-
disabled
|
|
1093
|
-
}) => /* @__PURE__ */ jsxs(
|
|
1094
|
-
Select,
|
|
1095
|
-
{
|
|
1096
|
-
value: value ? value : NONE,
|
|
1097
|
-
onValueChange: (v) => onValueChange(v === NONE ? "" : v),
|
|
1098
|
-
disabled,
|
|
1099
|
-
size: "small",
|
|
1100
|
-
children: [
|
|
1101
|
-
/* @__PURE__ */ jsx(Select.Trigger, { children: /* @__PURE__ */ jsx(Select.Value, { placeholder: "Not set" }) }),
|
|
1102
|
-
/* @__PURE__ */ jsxs(Select.Content, { children: [
|
|
1103
|
-
/* @__PURE__ */ jsx(Select.Item, { value: NONE, children: "Not set" }),
|
|
1104
|
-
options.map((o) => /* @__PURE__ */ jsx(Select.Item, { value: o.value, children: o.label }, o.value))
|
|
1105
|
-
] })
|
|
1106
|
-
]
|
|
1107
|
-
}
|
|
1108
|
-
);
|
|
1109
|
-
const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxs(
|
|
1110
|
-
"div",
|
|
1111
|
-
{
|
|
1112
|
-
className: clx(
|
|
1113
|
-
"flex items-center gap-2 rounded-lg border px-3 py-2",
|
|
1114
|
-
ok ? "border-ui-tag-green-border bg-ui-tag-green-bg" : "border-ui-tag-red-border bg-ui-tag-red-bg"
|
|
1115
|
-
),
|
|
1116
|
-
children: [
|
|
1117
|
-
/* @__PURE__ */ jsx(StatusBadge, { color: ok ? "green" : "red", children: ok ? "Ready" : "Missing" }),
|
|
1118
|
-
/* @__PURE__ */ jsx(Text, { children: label })
|
|
1119
|
-
]
|
|
1120
|
-
}
|
|
1121
|
-
);
|
|
1122
1135
|
const handle$1 = {
|
|
1123
|
-
breadcrumb: () => "
|
|
1136
|
+
breadcrumb: () => "Bulk sync"
|
|
1124
1137
|
};
|
|
1125
1138
|
const PARENT = "/settings/faire";
|
|
1126
1139
|
const STATUS_COLORS = {
|
|
@@ -1316,13 +1329,13 @@ const routeModule = {
|
|
|
1316
1329
|
handle: handle$3,
|
|
1317
1330
|
children: [
|
|
1318
1331
|
{
|
|
1319
|
-
Component:
|
|
1320
|
-
path: "/settings/faire/
|
|
1332
|
+
Component: FaireSyncSettingsDrawer,
|
|
1333
|
+
path: "/settings/faire/settings",
|
|
1321
1334
|
handle: handle$2
|
|
1322
1335
|
},
|
|
1323
1336
|
{
|
|
1324
|
-
Component:
|
|
1325
|
-
path: "/settings/faire/
|
|
1337
|
+
Component: FaireBulkPage,
|
|
1338
|
+
path: "/settings/faire/bulk",
|
|
1326
1339
|
handle: handle$1
|
|
1327
1340
|
}
|
|
1328
1341
|
]
|