@jytextiles/medusa-plugin-faire-store-sync 0.2.6 → 0.2.8
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/__tests__/faire-client.spec.js +98 -1
- package/.medusa/server/src/admin/index.js +312 -151
- package/.medusa/server/src/admin/index.mjs +314 -153
- package/.medusa/server/src/api/admin/faire/status/route.js +4 -4
- package/.medusa/server/src/api/admin/faire/taxonomy/route.js +12 -0
- package/.medusa/server/src/lib/faire-client.js +21 -1
- package/.medusa/server/src/modules/faire-sync/service.js +8 -1
- package/.medusa/server/src/workflows/sync-product-to-faire.js +11 -5
- package/package.json +1 -1
- package/src/__tests__/faire-client.spec.ts +107 -0
- package/src/admin/lib/api.ts +5 -1
- package/src/admin/routes/settings/faire/{bulk → @bulk}/page.tsx +17 -7
- package/src/admin/routes/settings/faire/{settings → @settings}/page.tsx +59 -21
- package/src/admin/routes/settings/faire/page.tsx +9 -2
- package/src/admin/widgets/faire-product-widget.tsx +182 -1
- package/src/api/admin/faire/status/route.ts +3 -3
- package/src/api/admin/faire/taxonomy/route.ts +10 -0
- package/src/lib/faire-client.ts +27 -0
- package/src/modules/faire-sync/service.ts +7 -0
- package/src/workflows/sync-product-to-faire.ts +16 -6
- /package/src/admin/routes/settings/faire/{bulk → @bulk}/use-bulk-product-columns.tsx +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
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,
|
|
3
|
+
import { toast, Container, Heading, Text, StatusBadge, Alert, Badge, Button, Tooltip, Drawer, Input, clx, createDataTableColumnHelper, createDataTableFilterHelper, useDataTable, Skeleton, DropdownMenu, DataTable, Label, Toaster, FocusModal, Switch, Select, IconButton } from "@medusajs/ui";
|
|
4
4
|
import { useQueryClient, useQuery, useMutation, keepPreviousData } from "@tanstack/react-query";
|
|
5
|
+
import { useState, useMemo, useEffect } from "react";
|
|
5
6
|
import Medusa from "@medusajs/js-sdk";
|
|
6
7
|
import { BuildingStorefront, ChevronDownMini, EllipsisHorizontal, ArrowPath, ArrowUpRightOnBox } from "@medusajs/icons";
|
|
7
|
-
import {
|
|
8
|
-
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
8
|
+
import { useNavigate, Outlet, useParams, useSearchParams } from "react-router-dom";
|
|
9
9
|
const sdk = new Medusa({
|
|
10
10
|
baseUrl: "/",
|
|
11
11
|
auth: {
|
|
@@ -60,6 +60,9 @@ 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: () => sdk.client.fetch(
|
|
64
|
+
"/admin/faire/taxonomy"
|
|
65
|
+
),
|
|
63
66
|
products: (opts = {}) => {
|
|
64
67
|
const query = {};
|
|
65
68
|
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
@@ -68,6 +71,7 @@ const faireApi = {
|
|
|
68
71
|
return sdk.client.fetch("/admin/faire/products", { query });
|
|
69
72
|
}
|
|
70
73
|
};
|
|
74
|
+
const MAX_RENDERED = 60;
|
|
71
75
|
const badgeColor = (status) => {
|
|
72
76
|
switch (status) {
|
|
73
77
|
case "synced":
|
|
@@ -83,7 +87,7 @@ const badgeColor = (status) => {
|
|
|
83
87
|
}
|
|
84
88
|
};
|
|
85
89
|
const FaireProductWidget = ({ data }) => {
|
|
86
|
-
var _a, _b, _c;
|
|
90
|
+
var _a, _b, _c, _d;
|
|
87
91
|
const queryClient = useQueryClient();
|
|
88
92
|
const productKey = ["faire", "product-status", data.id];
|
|
89
93
|
const statusQuery = useQuery({
|
|
@@ -126,6 +130,45 @@ const FaireProductWidget = ({ data }) => {
|
|
|
126
130
|
status: latest.status
|
|
127
131
|
} : null;
|
|
128
132
|
const notReady = connected && readiness && !readiness.ready_to_publish;
|
|
133
|
+
const metadata = data.metadata || {};
|
|
134
|
+
const productType = (_d = data.type) == null ? void 0 : _d.value;
|
|
135
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
136
|
+
const [pickerSearch, setPickerSearch] = useState("");
|
|
137
|
+
const [pinnedOverride, setPinnedOverride] = useState(
|
|
138
|
+
void 0
|
|
139
|
+
);
|
|
140
|
+
const taxonomyQuery = useQuery({
|
|
141
|
+
queryKey: ["faire", "taxonomy"],
|
|
142
|
+
enabled: pickerOpen,
|
|
143
|
+
queryFn: async () => (await faireApi.taxonomy()).taxonomy ?? []
|
|
144
|
+
});
|
|
145
|
+
const taxonomy = taxonomyQuery.data ?? [];
|
|
146
|
+
const pinnedId = pinnedOverride !== void 0 ? pinnedOverride ?? void 0 : metadata.faire_taxonomy_type_id;
|
|
147
|
+
const pinnedName = useMemo(() => {
|
|
148
|
+
var _a2;
|
|
149
|
+
if (!pinnedId) return void 0;
|
|
150
|
+
if (!/^tt_/.test(String(pinnedId))) return String(pinnedId);
|
|
151
|
+
return (_a2 = taxonomy.find((t) => t.id === pinnedId)) == null ? void 0 : _a2.name;
|
|
152
|
+
}, [pinnedId, taxonomy]);
|
|
153
|
+
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
|
+
const saveCategory = useMutation({
|
|
160
|
+
mutationFn: (value) => sdk.admin.product.update(data.id, {
|
|
161
|
+
metadata: { ...metadata, faire_taxonomy_type_id: value }
|
|
162
|
+
}),
|
|
163
|
+
onSuccess: (_res, value) => {
|
|
164
|
+
setPinnedOverride(value);
|
|
165
|
+
queryClient.invalidateQueries({ queryKey: ["product", data.id] });
|
|
166
|
+
setPickerOpen(false);
|
|
167
|
+
setPickerSearch("");
|
|
168
|
+
toast.success(value ? "Faire category set" : "Faire category cleared");
|
|
169
|
+
},
|
|
170
|
+
onError: (err) => toast.error("Failed to save category", { description: err == null ? void 0 : err.message })
|
|
171
|
+
});
|
|
129
172
|
return /* @__PURE__ */ jsxs(Container, { className: "divide-y p-0", children: [
|
|
130
173
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
131
174
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
@@ -152,6 +195,25 @@ const FaireProductWidget = ({ data }) => {
|
|
|
152
195
|
}
|
|
153
196
|
)
|
|
154
197
|
] }) }),
|
|
198
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-lg border px-3 py-2", children: [
|
|
199
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
200
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
201
|
+
/* @__PURE__ */ jsx(Text, { size: "small", weight: "plus", children: "Faire category" }),
|
|
202
|
+
/* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: resolvedSource.from === "fallback" ? "orange" : "grey", children: resolvedSource.from === "pinned" ? "Pinned" : resolvedSource.from === "type" ? "From Type" : resolvedSource.from === "metadata" ? "Metadata" : "Fallback" })
|
|
203
|
+
] }),
|
|
204
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: resolvedSource.label })
|
|
205
|
+
] }),
|
|
206
|
+
/* @__PURE__ */ jsx(
|
|
207
|
+
Button,
|
|
208
|
+
{
|
|
209
|
+
size: "small",
|
|
210
|
+
variant: "secondary",
|
|
211
|
+
disabled: !connected,
|
|
212
|
+
onClick: () => setPickerOpen(true),
|
|
213
|
+
children: pinnedId ? "Change" : "Set category"
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
] }),
|
|
155
217
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsx(
|
|
156
218
|
Tooltip,
|
|
157
219
|
{
|
|
@@ -168,7 +230,68 @@ const FaireProductWidget = ({ data }) => {
|
|
|
168
230
|
)
|
|
169
231
|
}
|
|
170
232
|
) })
|
|
171
|
-
] })
|
|
233
|
+
] }),
|
|
234
|
+
/* @__PURE__ */ jsx(Drawer, { open: pickerOpen, onOpenChange: setPickerOpen, children: /* @__PURE__ */ jsxs(Drawer.Content, { children: [
|
|
235
|
+
/* @__PURE__ */ jsxs(Drawer.Header, { children: [
|
|
236
|
+
/* @__PURE__ */ jsx(Drawer.Title, { children: "Pick a Faire category" }),
|
|
237
|
+
/* @__PURE__ */ jsx(Drawer.Description, { children: "Search Faire's product taxonomy and pin an exact category to this product." })
|
|
238
|
+
] }),
|
|
239
|
+
/* @__PURE__ */ jsxs(Drawer.Body, { className: "flex flex-col gap-3 overflow-hidden", children: [
|
|
240
|
+
/* @__PURE__ */ jsx(
|
|
241
|
+
Input,
|
|
242
|
+
{
|
|
243
|
+
type: "search",
|
|
244
|
+
autoFocus: true,
|
|
245
|
+
placeholder: "Search categories…",
|
|
246
|
+
value: pickerSearch,
|
|
247
|
+
onChange: (e) => setPickerSearch(e.target.value)
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
/* @__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…" }) : filteredTaxonomy.length === 0 ? /* @__PURE__ */ jsxs(Text, { size: "small", className: "text-ui-fg-subtle px-1 py-2", children: [
|
|
252
|
+
"No categories match “",
|
|
253
|
+
pickerSearch,
|
|
254
|
+
"”."
|
|
255
|
+
] }) : filteredTaxonomy.map((t) => /* @__PURE__ */ jsxs(
|
|
256
|
+
"button",
|
|
257
|
+
{
|
|
258
|
+
type: "button",
|
|
259
|
+
onClick: () => saveCategory.mutate(t.id),
|
|
260
|
+
disabled: saveCategory.isPending,
|
|
261
|
+
className: clx(
|
|
262
|
+
"flex items-center justify-between rounded-md px-3 py-2 text-left text-sm",
|
|
263
|
+
"hover:bg-ui-bg-base-hover",
|
|
264
|
+
t.id === pinnedId && "bg-ui-bg-highlight"
|
|
265
|
+
),
|
|
266
|
+
children: [
|
|
267
|
+
/* @__PURE__ */ jsx("span", { children: t.name }),
|
|
268
|
+
t.id === pinnedId && /* @__PURE__ */ jsx(Badge, { size: "2xsmall", color: "green", children: "Current" })
|
|
269
|
+
]
|
|
270
|
+
},
|
|
271
|
+
t.id
|
|
272
|
+
)),
|
|
273
|
+
!taxonomyQuery.isLoading && taxonomy.length > filteredTaxonomy.length && /* @__PURE__ */ jsxs(Text, { size: "xsmall", className: "text-ui-fg-muted px-1 py-1", children: [
|
|
274
|
+
"Showing ",
|
|
275
|
+
filteredTaxonomy.length,
|
|
276
|
+
" of ",
|
|
277
|
+
taxonomy.length,
|
|
278
|
+
" — refine your search to narrow down."
|
|
279
|
+
] })
|
|
280
|
+
] })
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxs(Drawer.Footer, { children: [
|
|
283
|
+
pinnedId && /* @__PURE__ */ jsx(
|
|
284
|
+
Button,
|
|
285
|
+
{
|
|
286
|
+
variant: "secondary",
|
|
287
|
+
onClick: () => saveCategory.mutate(null),
|
|
288
|
+
isLoading: saveCategory.isPending,
|
|
289
|
+
children: "Clear"
|
|
290
|
+
}
|
|
291
|
+
),
|
|
292
|
+
/* @__PURE__ */ jsx(Drawer.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: "Cancel" }) })
|
|
293
|
+
] })
|
|
294
|
+
] }) })
|
|
172
295
|
] });
|
|
173
296
|
};
|
|
174
297
|
defineWidgetConfig({
|
|
@@ -336,7 +459,8 @@ const FaireSettingsPage = () => {
|
|
|
336
459
|
/* @__PURE__ */ jsxs(Container, { className: "p-0", children: [
|
|
337
460
|
/* @__PURE__ */ jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-32" }) }),
|
|
338
461
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3 px-6 pb-6", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) })
|
|
339
|
-
] })
|
|
462
|
+
] }),
|
|
463
|
+
/* @__PURE__ */ jsx(Outlet, {})
|
|
340
464
|
] });
|
|
341
465
|
}
|
|
342
466
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
@@ -464,6 +588,7 @@ const FaireSettingsPage = () => {
|
|
|
464
588
|
)
|
|
465
589
|
] })
|
|
466
590
|
] }) }),
|
|
591
|
+
/* @__PURE__ */ jsx(Outlet, {}),
|
|
467
592
|
/* @__PURE__ */ jsx(Toaster, {})
|
|
468
593
|
] });
|
|
469
594
|
};
|
|
@@ -686,10 +811,7 @@ const FaireBulkPage = () => {
|
|
|
686
811
|
search: { state: search, onSearchChange: setSearch }
|
|
687
812
|
});
|
|
688
813
|
return /* @__PURE__ */ jsxs(RouteFocusModal, { prev: "/settings/faire", children: [
|
|
689
|
-
/* @__PURE__ */ jsx(RouteFocusModal.Header, { children: /* @__PURE__ */
|
|
690
|
-
/* @__PURE__ */ jsx(Heading, { children: "Bulk sync products to Faire" }),
|
|
691
|
-
/* @__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." })
|
|
692
|
-
] }) }),
|
|
814
|
+
/* @__PURE__ */ jsx(RouteFocusModal.Header, { children: /* @__PURE__ */ jsx(RouteFocusModal.Title, { asChild: true, children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Bulk sync" }) }) }),
|
|
693
815
|
/* @__PURE__ */ jsx(RouteFocusModal.Body, { className: "flex flex-1 flex-col overflow-hidden p-0", children: /* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
|
|
694
816
|
/* @__PURE__ */ jsxs(DataTable.Toolbar, { className: "flex flex-col gap-y-3 border-b px-6 py-4", children: [
|
|
695
817
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2 md:flex-row md:items-center md:justify-between", children: [
|
|
@@ -729,6 +851,22 @@ const FaireBulkPage = () => {
|
|
|
729
851
|
},
|
|
730
852
|
children: selectAllMatching ? `All ${count} selected` : `Select all ${count}`
|
|
731
853
|
}
|
|
854
|
+
),
|
|
855
|
+
/* @__PURE__ */ jsx(
|
|
856
|
+
Button,
|
|
857
|
+
{
|
|
858
|
+
size: "small",
|
|
859
|
+
onClick: () => {
|
|
860
|
+
if (!connected) {
|
|
861
|
+
toast.error("Faire is not connected");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
pushMutation.mutate();
|
|
865
|
+
},
|
|
866
|
+
disabled: !connected || targetCount === 0,
|
|
867
|
+
isLoading: pushMutation.isPending,
|
|
868
|
+
children: "Push to Faire"
|
|
869
|
+
}
|
|
732
870
|
)
|
|
733
871
|
] })
|
|
734
872
|
] }),
|
|
@@ -777,127 +915,14 @@ const FaireBulkPage = () => {
|
|
|
777
915
|
const handle$2 = {
|
|
778
916
|
breadcrumb: () => "Bulk sync"
|
|
779
917
|
};
|
|
780
|
-
const PARENT$1 = "/settings/faire";
|
|
781
|
-
const STATUS_COLORS = {
|
|
782
|
-
success: "green",
|
|
783
|
-
failed: "red",
|
|
784
|
-
draft: "orange",
|
|
785
|
-
pending: "grey",
|
|
786
|
-
syncing: "grey"
|
|
787
|
-
};
|
|
788
|
-
const FaireSyncDetailPage = () => {
|
|
789
|
-
const { id } = useParams();
|
|
790
|
-
const navigate = useNavigate();
|
|
791
|
-
const [record, setRecord] = useState(null);
|
|
792
|
-
const [loading, setLoading] = useState(true);
|
|
793
|
-
const [retrying, setRetrying] = useState(false);
|
|
794
|
-
const [error, setError] = useState(null);
|
|
795
|
-
useEffect(() => {
|
|
796
|
-
if (!id) return;
|
|
797
|
-
setLoading(true);
|
|
798
|
-
faireApi.getSync(id).then((res) => {
|
|
799
|
-
setRecord(res.sync || res);
|
|
800
|
-
}).catch((err) => setError(err.message || "Failed to load sync record")).finally(() => setLoading(false));
|
|
801
|
-
}, [id]);
|
|
802
|
-
const handleRetry = async () => {
|
|
803
|
-
if (!record) return;
|
|
804
|
-
setRetrying(true);
|
|
805
|
-
setError(null);
|
|
806
|
-
try {
|
|
807
|
-
await faireApi.retrySync(record.id);
|
|
808
|
-
const updated = await faireApi.getSync(record.id).catch(() => null);
|
|
809
|
-
if (updated) setRecord(updated.sync || updated);
|
|
810
|
-
} catch (err) {
|
|
811
|
-
setError(err.message || "Retry failed");
|
|
812
|
-
} finally {
|
|
813
|
-
setRetrying(false);
|
|
814
|
-
}
|
|
815
|
-
};
|
|
816
|
-
const warnings = (record == null ? void 0 : record.error_message) ? record.error_message.split(" | ") : [];
|
|
817
|
-
return /* @__PURE__ */ jsxs(RouteFocusModal, { prev: PARENT$1, children: [
|
|
818
|
-
/* @__PURE__ */ jsxs(RouteFocusModal.Header, { children: [
|
|
819
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
820
|
-
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Sync record" }),
|
|
821
|
-
record && /* @__PURE__ */ jsx(StatusBadge, { color: STATUS_COLORS[record.status] || "grey", children: record.status })
|
|
822
|
-
] }),
|
|
823
|
-
record && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
824
|
-
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", disabled: retrying, children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
|
|
825
|
-
/* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
|
|
826
|
-
/* @__PURE__ */ jsxs(
|
|
827
|
-
DropdownMenu.Item,
|
|
828
|
-
{
|
|
829
|
-
className: "gap-x-2",
|
|
830
|
-
onClick: handleRetry,
|
|
831
|
-
disabled: retrying,
|
|
832
|
-
children: [
|
|
833
|
-
/* @__PURE__ */ jsx(ArrowPath, { className: "text-ui-fg-subtle" }),
|
|
834
|
-
retrying ? "Re-syncing…" : "Re-sync product"
|
|
835
|
-
]
|
|
836
|
-
}
|
|
837
|
-
),
|
|
838
|
-
record.product_url && /* @__PURE__ */ jsxs(
|
|
839
|
-
DropdownMenu.Item,
|
|
840
|
-
{
|
|
841
|
-
className: "gap-x-2",
|
|
842
|
-
onClick: () => window.open(record.product_url, "_blank"),
|
|
843
|
-
children: [
|
|
844
|
-
/* @__PURE__ */ jsx(ArrowUpRightOnBox, { className: "text-ui-fg-subtle" }),
|
|
845
|
-
"Open on Faire"
|
|
846
|
-
]
|
|
847
|
-
}
|
|
848
|
-
)
|
|
849
|
-
] })
|
|
850
|
-
] }) })
|
|
851
|
-
] }),
|
|
852
|
-
/* @__PURE__ */ jsxs(RouteFocusModal.Body, { className: "flex flex-col gap-4 overflow-y-auto p-6", children: [
|
|
853
|
-
error && /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text", children: error }),
|
|
854
|
-
loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }) : !record ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-4", children: [
|
|
855
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Sync record not found" }),
|
|
856
|
-
/* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => navigate(PARENT$1), children: "Back to Faire settings" })
|
|
857
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
858
|
-
/* @__PURE__ */ jsxs("div", { className: "border-ui-border-base grid grid-cols-2 gap-4 rounded-lg border p-4", children: [
|
|
859
|
-
/* @__PURE__ */ jsx(Detail, { label: "Record id", value: record.id, mono: true }),
|
|
860
|
-
/* @__PURE__ */ jsx(Detail, { label: "Product", value: record.product_id, mono: true }),
|
|
861
|
-
/* @__PURE__ */ jsx(Detail, { label: "Action", value: record.action }),
|
|
862
|
-
/* @__PURE__ */ jsx(Detail, { label: "Faire product token", value: record.product_token || "—", mono: true }),
|
|
863
|
-
/* @__PURE__ */ jsx(Detail, { label: "State", value: record.product_state || "—" }),
|
|
864
|
-
/* @__PURE__ */ jsx(Detail, { label: "Published", value: record.published ? "Yes" : "No" }),
|
|
865
|
-
/* @__PURE__ */ jsx(
|
|
866
|
-
Detail,
|
|
867
|
-
{
|
|
868
|
-
label: "Synced at",
|
|
869
|
-
value: record.synced_at ? new Date(record.synced_at).toLocaleString() : "—"
|
|
870
|
-
}
|
|
871
|
-
)
|
|
872
|
-
] }),
|
|
873
|
-
warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-ui-border-base flex flex-col gap-2 rounded-lg border p-4", children: [
|
|
874
|
-
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Issues" }),
|
|
875
|
-
warnings.map((w, i) => /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text text-sm", children: w }, i))
|
|
876
|
-
] })
|
|
877
|
-
] })
|
|
878
|
-
] }),
|
|
879
|
-
/* @__PURE__ */ jsx(Toaster, {})
|
|
880
|
-
] });
|
|
881
|
-
};
|
|
882
|
-
const Detail = ({
|
|
883
|
-
label,
|
|
884
|
-
value,
|
|
885
|
-
mono
|
|
886
|
-
}) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
887
|
-
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
888
|
-
/* @__PURE__ */ jsx(Text, { className: mono ? "font-mono text-xs" : "", children: value })
|
|
889
|
-
] });
|
|
890
|
-
const handle$1 = {
|
|
891
|
-
breadcrumb: () => "Sync record"
|
|
892
|
-
};
|
|
893
918
|
const STATUS_KEY = ["faire", "status"];
|
|
894
|
-
const
|
|
919
|
+
const TAXONOMY_KEY = ["faire", "taxonomy"];
|
|
920
|
+
const PARENT$1 = "/settings/faire";
|
|
895
921
|
const NONE = "__none__";
|
|
896
922
|
const cleanSettings = (form) => {
|
|
897
923
|
const out = { ...form };
|
|
898
924
|
for (const key of [
|
|
899
925
|
"default_brand_id",
|
|
900
|
-
"default_shipping_policy_id",
|
|
901
926
|
"default_category"
|
|
902
927
|
]) {
|
|
903
928
|
if (!out[key] || out[key] === NONE) out[key] = null;
|
|
@@ -917,7 +942,7 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
917
942
|
const [form, setForm] = useState({});
|
|
918
943
|
const close = () => {
|
|
919
944
|
setOpen(false);
|
|
920
|
-
navigate(PARENT);
|
|
945
|
+
navigate(PARENT$1);
|
|
921
946
|
};
|
|
922
947
|
const statusQuery = useQuery({
|
|
923
948
|
queryKey: STATUS_KEY,
|
|
@@ -925,6 +950,11 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
925
950
|
});
|
|
926
951
|
const status = statusQuery.data;
|
|
927
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 ?? [];
|
|
928
958
|
useEffect(() => {
|
|
929
959
|
if (status == null ? void 0 : status.settings) setForm(status.settings);
|
|
930
960
|
}, [status == null ? void 0 : status.settings]);
|
|
@@ -953,7 +983,7 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
953
983
|
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.connected, label: "Faire connected" }),
|
|
954
984
|
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.brand, label: "Brand configured" }),
|
|
955
985
|
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.wholesale_pricing, label: "Wholesale pricing" }),
|
|
956
|
-
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.
|
|
986
|
+
/* @__PURE__ */ jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.taxonomy, label: "Default category" })
|
|
957
987
|
] })
|
|
958
988
|
] }),
|
|
959
989
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
@@ -963,9 +993,8 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
963
993
|
] }),
|
|
964
994
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-4", children: [
|
|
965
995
|
/* @__PURE__ */ jsx(Field, { label: "Brand ID", children: /* @__PURE__ */ jsx(
|
|
966
|
-
|
|
996
|
+
Input,
|
|
967
997
|
{
|
|
968
|
-
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
969
998
|
value: String(form.default_brand_id ?? ""),
|
|
970
999
|
onChange: (e) => setForm({ ...form, default_brand_id: e.target.value || null }),
|
|
971
1000
|
disabled: !connected,
|
|
@@ -973,10 +1002,9 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
973
1002
|
}
|
|
974
1003
|
) }),
|
|
975
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(
|
|
976
|
-
|
|
1005
|
+
Input,
|
|
977
1006
|
{
|
|
978
1007
|
type: "number",
|
|
979
|
-
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
980
1008
|
value: String(form.default_wholesale_markup_percent ?? ""),
|
|
981
1009
|
onChange: (e) => setForm({
|
|
982
1010
|
...form,
|
|
@@ -987,10 +1015,9 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
987
1015
|
}
|
|
988
1016
|
) }) }),
|
|
989
1017
|
/* @__PURE__ */ jsx(Field, { label: "Default min order quantity", children: /* @__PURE__ */ jsx(
|
|
990
|
-
|
|
1018
|
+
Input,
|
|
991
1019
|
{
|
|
992
1020
|
type: "number",
|
|
993
|
-
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
994
1021
|
value: String(form.default_min_order_quantity ?? 1),
|
|
995
1022
|
onChange: (e) => setForm({
|
|
996
1023
|
...form,
|
|
@@ -1000,10 +1027,9 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
1000
1027
|
}
|
|
1001
1028
|
) }),
|
|
1002
1029
|
/* @__PURE__ */ jsx(Field, { label: "Default lead time (days)", children: /* @__PURE__ */ jsx(
|
|
1003
|
-
|
|
1030
|
+
Input,
|
|
1004
1031
|
{
|
|
1005
1032
|
type: "number",
|
|
1006
|
-
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
1007
1033
|
value: String(form.default_lead_time_days ?? ""),
|
|
1008
1034
|
onChange: (e) => setForm({
|
|
1009
1035
|
...form,
|
|
@@ -1013,16 +1039,15 @@ const FaireSyncSettingsDrawer = () => {
|
|
|
1013
1039
|
placeholder: "e.g. 14"
|
|
1014
1040
|
}
|
|
1015
1041
|
) }),
|
|
1016
|
-
/* @__PURE__ */ jsx(Field, { label: "
|
|
1017
|
-
|
|
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,
|
|
1018
1044
|
{
|
|
1019
|
-
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
1020
1045
|
value: String(form.default_category ?? ""),
|
|
1021
|
-
|
|
1046
|
+
onValueChange: (v) => setForm({ ...form, default_category: v || null }),
|
|
1022
1047
|
disabled: !connected,
|
|
1023
|
-
|
|
1048
|
+
options: taxonomy.map((t) => ({ value: t.id, label: t.name }))
|
|
1024
1049
|
}
|
|
1025
|
-
) }),
|
|
1050
|
+
) }) }),
|
|
1026
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(
|
|
1027
1052
|
Switch,
|
|
1028
1053
|
{
|
|
@@ -1060,6 +1085,27 @@ const Field = ({ label, children }) => /* @__PURE__ */ jsxs("div", { className:
|
|
|
1060
1085
|
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
1061
1086
|
children
|
|
1062
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
|
+
);
|
|
1063
1109
|
const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxs(
|
|
1064
1110
|
"div",
|
|
1065
1111
|
{
|
|
@@ -1073,9 +1119,122 @@ const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxs(
|
|
|
1073
1119
|
]
|
|
1074
1120
|
}
|
|
1075
1121
|
);
|
|
1076
|
-
const handle = {
|
|
1122
|
+
const handle$1 = {
|
|
1077
1123
|
breadcrumb: () => "Sync settings"
|
|
1078
1124
|
};
|
|
1125
|
+
const PARENT = "/settings/faire";
|
|
1126
|
+
const STATUS_COLORS = {
|
|
1127
|
+
success: "green",
|
|
1128
|
+
failed: "red",
|
|
1129
|
+
draft: "orange",
|
|
1130
|
+
pending: "grey",
|
|
1131
|
+
syncing: "grey"
|
|
1132
|
+
};
|
|
1133
|
+
const FaireSyncDetailPage = () => {
|
|
1134
|
+
const { id } = useParams();
|
|
1135
|
+
const navigate = useNavigate();
|
|
1136
|
+
const [record, setRecord] = useState(null);
|
|
1137
|
+
const [loading, setLoading] = useState(true);
|
|
1138
|
+
const [retrying, setRetrying] = useState(false);
|
|
1139
|
+
const [error, setError] = useState(null);
|
|
1140
|
+
useEffect(() => {
|
|
1141
|
+
if (!id) return;
|
|
1142
|
+
setLoading(true);
|
|
1143
|
+
faireApi.getSync(id).then((res) => {
|
|
1144
|
+
setRecord(res.sync || res);
|
|
1145
|
+
}).catch((err) => setError(err.message || "Failed to load sync record")).finally(() => setLoading(false));
|
|
1146
|
+
}, [id]);
|
|
1147
|
+
const handleRetry = async () => {
|
|
1148
|
+
if (!record) return;
|
|
1149
|
+
setRetrying(true);
|
|
1150
|
+
setError(null);
|
|
1151
|
+
try {
|
|
1152
|
+
await faireApi.retrySync(record.id);
|
|
1153
|
+
const updated = await faireApi.getSync(record.id).catch(() => null);
|
|
1154
|
+
if (updated) setRecord(updated.sync || updated);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
setError(err.message || "Retry failed");
|
|
1157
|
+
} finally {
|
|
1158
|
+
setRetrying(false);
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
const warnings = (record == null ? void 0 : record.error_message) ? record.error_message.split(" | ") : [];
|
|
1162
|
+
return /* @__PURE__ */ jsxs(RouteFocusModal, { prev: PARENT, children: [
|
|
1163
|
+
/* @__PURE__ */ jsxs(RouteFocusModal.Header, { children: [
|
|
1164
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1165
|
+
/* @__PURE__ */ jsx(Heading, { level: "h1", children: "Sync record" }),
|
|
1166
|
+
record && /* @__PURE__ */ jsx(StatusBadge, { color: STATUS_COLORS[record.status] || "grey", children: record.status })
|
|
1167
|
+
] }),
|
|
1168
|
+
record && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
1169
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", disabled: retrying, children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }),
|
|
1170
|
+
/* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
|
|
1171
|
+
/* @__PURE__ */ jsxs(
|
|
1172
|
+
DropdownMenu.Item,
|
|
1173
|
+
{
|
|
1174
|
+
className: "gap-x-2",
|
|
1175
|
+
onClick: handleRetry,
|
|
1176
|
+
disabled: retrying,
|
|
1177
|
+
children: [
|
|
1178
|
+
/* @__PURE__ */ jsx(ArrowPath, { className: "text-ui-fg-subtle" }),
|
|
1179
|
+
retrying ? "Re-syncing…" : "Re-sync product"
|
|
1180
|
+
]
|
|
1181
|
+
}
|
|
1182
|
+
),
|
|
1183
|
+
record.product_url && /* @__PURE__ */ jsxs(
|
|
1184
|
+
DropdownMenu.Item,
|
|
1185
|
+
{
|
|
1186
|
+
className: "gap-x-2",
|
|
1187
|
+
onClick: () => window.open(record.product_url, "_blank"),
|
|
1188
|
+
children: [
|
|
1189
|
+
/* @__PURE__ */ jsx(ArrowUpRightOnBox, { className: "text-ui-fg-subtle" }),
|
|
1190
|
+
"Open on Faire"
|
|
1191
|
+
]
|
|
1192
|
+
}
|
|
1193
|
+
)
|
|
1194
|
+
] })
|
|
1195
|
+
] }) })
|
|
1196
|
+
] }),
|
|
1197
|
+
/* @__PURE__ */ jsxs(RouteFocusModal.Body, { className: "flex flex-col gap-4 overflow-y-auto p-6", children: [
|
|
1198
|
+
error && /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text", children: error }),
|
|
1199
|
+
loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { className: "h-10 w-full" }, i)) }) : !record ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-start gap-4", children: [
|
|
1200
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Sync record not found" }),
|
|
1201
|
+
/* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", onClick: () => navigate(PARENT), children: "Back to Faire settings" })
|
|
1202
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1203
|
+
/* @__PURE__ */ jsxs("div", { className: "border-ui-border-base grid grid-cols-2 gap-4 rounded-lg border p-4", children: [
|
|
1204
|
+
/* @__PURE__ */ jsx(Detail, { label: "Record id", value: record.id, mono: true }),
|
|
1205
|
+
/* @__PURE__ */ jsx(Detail, { label: "Product", value: record.product_id, mono: true }),
|
|
1206
|
+
/* @__PURE__ */ jsx(Detail, { label: "Action", value: record.action }),
|
|
1207
|
+
/* @__PURE__ */ jsx(Detail, { label: "Faire product token", value: record.product_token || "—", mono: true }),
|
|
1208
|
+
/* @__PURE__ */ jsx(Detail, { label: "State", value: record.product_state || "—" }),
|
|
1209
|
+
/* @__PURE__ */ jsx(Detail, { label: "Published", value: record.published ? "Yes" : "No" }),
|
|
1210
|
+
/* @__PURE__ */ jsx(
|
|
1211
|
+
Detail,
|
|
1212
|
+
{
|
|
1213
|
+
label: "Synced at",
|
|
1214
|
+
value: record.synced_at ? new Date(record.synced_at).toLocaleString() : "—"
|
|
1215
|
+
}
|
|
1216
|
+
)
|
|
1217
|
+
] }),
|
|
1218
|
+
warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "border-ui-border-base flex flex-col gap-2 rounded-lg border p-4", children: [
|
|
1219
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Issues" }),
|
|
1220
|
+
warnings.map((w, i) => /* @__PURE__ */ jsx(Text, { className: "text-ui-tag-red-text text-sm", children: w }, i))
|
|
1221
|
+
] })
|
|
1222
|
+
] })
|
|
1223
|
+
] }),
|
|
1224
|
+
/* @__PURE__ */ jsx(Toaster, {})
|
|
1225
|
+
] });
|
|
1226
|
+
};
|
|
1227
|
+
const Detail = ({
|
|
1228
|
+
label,
|
|
1229
|
+
value,
|
|
1230
|
+
mono
|
|
1231
|
+
}) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
1232
|
+
/* @__PURE__ */ jsx(Label, { children: label }),
|
|
1233
|
+
/* @__PURE__ */ jsx(Text, { className: mono ? "font-mono text-xs" : "", children: value })
|
|
1234
|
+
] });
|
|
1235
|
+
const handle = {
|
|
1236
|
+
breadcrumb: () => "Sync record"
|
|
1237
|
+
};
|
|
1079
1238
|
const EXCHANGED_PREFIX = "faire:oauth:exchanged:";
|
|
1080
1239
|
const FaireOauthCallback = () => {
|
|
1081
1240
|
const [params] = useSearchParams();
|
|
@@ -1154,21 +1313,23 @@ const routeModule = {
|
|
|
1154
1313
|
{
|
|
1155
1314
|
Component: FaireSettingsPage,
|
|
1156
1315
|
path: "/settings/faire",
|
|
1157
|
-
handle: handle$3
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1316
|
+
handle: handle$3,
|
|
1317
|
+
children: [
|
|
1318
|
+
{
|
|
1319
|
+
Component: FaireBulkPage,
|
|
1320
|
+
path: "/settings/faire/bulk",
|
|
1321
|
+
handle: handle$2
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
Component: FaireSyncSettingsDrawer,
|
|
1325
|
+
path: "/settings/faire/settings",
|
|
1326
|
+
handle: handle$1
|
|
1327
|
+
}
|
|
1328
|
+
]
|
|
1163
1329
|
},
|
|
1164
1330
|
{
|
|
1165
1331
|
Component: FaireSyncDetailPage,
|
|
1166
1332
|
path: "/settings/faire/:id",
|
|
1167
|
-
handle: handle$1
|
|
1168
|
-
},
|
|
1169
|
-
{
|
|
1170
|
-
Component: FaireSyncSettingsDrawer,
|
|
1171
|
-
path: "/settings/faire/settings",
|
|
1172
1333
|
handle
|
|
1173
1334
|
},
|
|
1174
1335
|
{
|