@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.
@@ -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, Tooltip, Button, createDataTableColumnHelper, toast, createDataTableFilterHelper, useDataTable, Skeleton, DropdownMenu, DataTable, Drawer, Label, Input, Toaster, FocusModal, Badge, IconButton, Switch, clx } from "@medusajs/ui";
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 { useState, useEffect, useMemo } from "react";
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__ */ jsxs("div", { className: "flex flex-col", children: [
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 PARENT = "/settings/faire";
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.shipping_policy, label: "Shipping policy" })
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
- "input",
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
- "input",
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
- "input",
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
- "input",
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: "Default category", children: /* @__PURE__ */ jsx(
1017
- "input",
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
- onChange: (e) => setForm({ ...form, default_category: e.target.value || null }),
1046
+ onValueChange: (v) => setForm({ ...form, default_category: v || null }),
1022
1047
  disabled: !connected,
1023
- placeholder: "e.g. Home Decor"
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
- Component: FaireBulkPage,
1161
- path: "/settings/faire/bulk",
1162
- handle: handle$2
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
  {