@jytextiles/medusa-plugin-faire-store-sync 0.1.0
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__/ingest-faire-order-support.spec.js +74 -0
- package/.medusa/server/src/__tests__/webhook.spec.js +77 -0
- package/.medusa/server/src/admin/index.js +960 -0
- package/.medusa/server/src/admin/index.mjs +959 -0
- package/.medusa/server/src/api/admin/faire/auth/authorize/route.js +12 -0
- package/.medusa/server/src/api/admin/faire/auth/callback/route.js +25 -0
- package/.medusa/server/src/api/admin/faire/auth/disconnect/route.js +12 -0
- package/.medusa/server/src/api/admin/faire/brand/route.js +14 -0
- package/.medusa/server/src/api/admin/faire/ingest/orders/[batchId]/route.js +27 -0
- package/.medusa/server/src/api/admin/faire/ingest/orders/route.js +20 -0
- package/.medusa/server/src/api/admin/faire/products/route.js +16 -0
- package/.medusa/server/src/api/admin/faire/settings/route.js +19 -0
- package/.medusa/server/src/api/admin/faire/status/product/[id]/route.js +30 -0
- package/.medusa/server/src/api/admin/faire/status/route.js +46 -0
- package/.medusa/server/src/api/admin/faire/sync/bulk/[batchId]/route.js +29 -0
- package/.medusa/server/src/api/admin/faire/sync/bulk/route.js +20 -0
- package/.medusa/server/src/api/admin/faire/sync/product/[id]/route.js +14 -0
- package/.medusa/server/src/api/admin/faire/syncs/[id]/route.js +32 -0
- package/.medusa/server/src/api/admin/faire/syncs/route.js +18 -0
- package/.medusa/server/src/api/webhooks/faire/route.js +125 -0
- package/.medusa/server/src/jobs/pull-faire-orders.js +32 -0
- package/.medusa/server/src/jobs/refresh-faire-token.js +25 -0
- package/.medusa/server/src/lib/faire-client.js +321 -0
- package/.medusa/server/src/lib/types.js +10 -0
- package/.medusa/server/src/lib/webhook.js +58 -0
- package/.medusa/server/src/links/product-faire-sync-link.js +57 -0
- package/.medusa/server/src/modules/faire-sync/index.js +15 -0
- package/.medusa/server/src/modules/faire-sync/migrations/Migration20260707211346.js +34 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-order.js +21 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-account.js +17 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-batch.js +18 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-record.js +22 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-sync-settings.js +26 -0
- package/.medusa/server/src/modules/faire-sync/models/faire-webhook-event.js +21 -0
- package/.medusa/server/src/modules/faire-sync/service.js +222 -0
- package/.medusa/server/src/subscribers/faire-inventory-sync.js +87 -0
- package/.medusa/server/src/subscribers/faire-listing-events.js +46 -0
- package/.medusa/server/src/subscribers/faire-order-ingest.js +54 -0
- package/.medusa/server/src/subscribers/faire-order-lifecycle.js +71 -0
- package/.medusa/server/src/subscribers/faire-product-status-sync.js +71 -0
- package/.medusa/server/src/workflows/index.js +21 -0
- package/.medusa/server/src/workflows/ingest-faire-order-support.js +76 -0
- package/.medusa/server/src/workflows/ingest-faire-order.js +148 -0
- package/.medusa/server/src/workflows/ingest-faire-orders-bulk.js +124 -0
- package/.medusa/server/src/workflows/refresh-faire-token.js +20 -0
- package/.medusa/server/src/workflows/sync-product-to-faire.js +262 -0
- package/.medusa/server/src/workflows/sync-products-to-faire.js +90 -0
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/package.json +84 -0
- package/scripts/postbuild.js +36 -0
- package/src/__tests__/ingest-faire-order-support.spec.ts +77 -0
- package/src/__tests__/webhook.spec.ts +81 -0
- package/src/admin/lib/api.ts +88 -0
- package/src/admin/routes/settings/faire/[id]/page.tsx +184 -0
- package/src/admin/routes/settings/faire/hooks/use-faire-sync-columns.tsx +80 -0
- package/src/admin/routes/settings/faire/page.tsx +445 -0
- package/src/admin/routes/settings/faire/settings/page.tsx +286 -0
- package/src/admin/routes/settings/oauth/faire/callback/page.tsx +67 -0
- package/src/admin/widgets/faire-product-widget.tsx +180 -0
- package/src/api/admin/faire/auth/authorize/route.ts +10 -0
- package/src/api/admin/faire/auth/callback/route.ts +29 -0
- package/src/api/admin/faire/auth/disconnect/route.ts +10 -0
- package/src/api/admin/faire/brand/route.ts +12 -0
- package/src/api/admin/faire/ingest/orders/[batchId]/route.ts +31 -0
- package/src/api/admin/faire/ingest/orders/route.ts +29 -0
- package/src/api/admin/faire/products/route.ts +14 -0
- package/src/api/admin/faire/settings/route.ts +17 -0
- package/src/api/admin/faire/status/product/[id]/route.ts +30 -0
- package/src/api/admin/faire/status/route.ts +47 -0
- package/src/api/admin/faire/sync/bulk/[batchId]/route.ts +33 -0
- package/src/api/admin/faire/sync/bulk/route.ts +27 -0
- package/src/api/admin/faire/sync/product/[id]/route.ts +13 -0
- package/src/api/admin/faire/syncs/[id]/route.ts +32 -0
- package/src/api/admin/faire/syncs/route.ts +17 -0
- package/src/api/webhooks/faire/route.ts +133 -0
- package/src/jobs/pull-faire-orders.ts +30 -0
- package/src/jobs/refresh-faire-token.ts +23 -0
- package/src/lib/faire-client.ts +436 -0
- package/src/lib/types.ts +117 -0
- package/src/lib/webhook.ts +85 -0
- package/src/links/product-faire-sync-link.ts +56 -0
- package/src/modules/faire-sync/index.ts +11 -0
- package/src/modules/faire-sync/migrations/.snapshot-medusa-faire-sync.json +1486 -0
- package/src/modules/faire-sync/migrations/Migration20260707211346.ts +43 -0
- package/src/modules/faire-sync/models/faire-order.ts +20 -0
- package/src/modules/faire-sync/models/faire-sync-account.ts +16 -0
- package/src/modules/faire-sync/models/faire-sync-batch.ts +17 -0
- package/src/modules/faire-sync/models/faire-sync-record.ts +21 -0
- package/src/modules/faire-sync/models/faire-sync-settings.ts +25 -0
- package/src/modules/faire-sync/models/faire-webhook-event.ts +20 -0
- package/src/modules/faire-sync/service.ts +252 -0
- package/src/subscribers/faire-inventory-sync.ts +97 -0
- package/src/subscribers/faire-listing-events.ts +48 -0
- package/src/subscribers/faire-order-ingest.ts +61 -0
- package/src/subscribers/faire-order-lifecycle.ts +81 -0
- package/src/subscribers/faire-product-status-sync.ts +76 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/ingest-faire-order-support.ts +96 -0
- package/src/workflows/ingest-faire-order.ts +212 -0
- package/src/workflows/ingest-faire-orders-bulk.ts +168 -0
- package/src/workflows/refresh-faire-token.ts +28 -0
- package/src/workflows/sync-product-to-faire.ts +375 -0
- package/src/workflows/sync-products-to-faire.ts +133 -0
|
@@ -0,0 +1,960 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
3
|
+
const adminSdk = require("@medusajs/admin-sdk");
|
|
4
|
+
const ui = require("@medusajs/ui");
|
|
5
|
+
const reactQuery = require("@tanstack/react-query");
|
|
6
|
+
const Medusa = require("@medusajs/js-sdk");
|
|
7
|
+
const icons = require("@medusajs/icons");
|
|
8
|
+
const react = require("react");
|
|
9
|
+
const reactRouterDom = require("react-router-dom");
|
|
10
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
11
|
+
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
12
|
+
const sdk = new Medusa__default.default({
|
|
13
|
+
baseUrl: "/",
|
|
14
|
+
auth: {
|
|
15
|
+
type: "session"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const faireApi = {
|
|
19
|
+
status: () => sdk.client.fetch("/admin/faire/status"),
|
|
20
|
+
getSettings: () => sdk.client.fetch("/admin/faire/settings"),
|
|
21
|
+
saveSettings: (settings) => sdk.client.fetch("/admin/faire/settings", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
body: settings
|
|
24
|
+
}),
|
|
25
|
+
authorize: () => sdk.client.fetch(
|
|
26
|
+
"/admin/faire/auth/authorize"
|
|
27
|
+
),
|
|
28
|
+
callback: (code, state) => sdk.client.fetch("/admin/faire/auth/callback", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: { code, state }
|
|
31
|
+
}),
|
|
32
|
+
disconnect: () => sdk.client.fetch("/admin/faire/auth/disconnect", { method: "POST" }),
|
|
33
|
+
syncProduct: (id) => sdk.client.fetch(`/admin/faire/sync/product/${id}`, { method: "POST" }),
|
|
34
|
+
productStatus: (id) => sdk.client.fetch(`/admin/faire/status/product/${id}`),
|
|
35
|
+
syncBulk: (product_ids) => sdk.client.fetch(
|
|
36
|
+
"/admin/faire/sync/bulk",
|
|
37
|
+
{
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: { product_ids }
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
bulkStatus: (batchId) => sdk.client.fetch(
|
|
43
|
+
`/admin/faire/sync/bulk/${batchId}`
|
|
44
|
+
),
|
|
45
|
+
ingestOrders: (limit) => sdk.client.fetch(
|
|
46
|
+
"/admin/faire/ingest/orders",
|
|
47
|
+
{ method: "POST", body: limit != null ? { limit } : {} }
|
|
48
|
+
),
|
|
49
|
+
ingestStatus: (batchId) => sdk.client.fetch(
|
|
50
|
+
`/admin/faire/ingest/orders/${batchId}`
|
|
51
|
+
),
|
|
52
|
+
listSyncs: (opts = {}) => {
|
|
53
|
+
const query = {};
|
|
54
|
+
if (opts.take !== void 0) query.take = String(opts.take);
|
|
55
|
+
if (opts.skip !== void 0) query.skip = String(opts.skip);
|
|
56
|
+
if (opts.status) query.status = opts.status;
|
|
57
|
+
return sdk.client.fetch("/admin/faire/syncs", { query });
|
|
58
|
+
},
|
|
59
|
+
getSync: (id) => sdk.client.fetch(`/admin/faire/syncs/${id}`),
|
|
60
|
+
retrySync: (id) => sdk.client.fetch(`/admin/faire/syncs/${id}`, { method: "POST" }),
|
|
61
|
+
brand: () => sdk.client.fetch("/admin/faire/brand"),
|
|
62
|
+
products: (opts = {}) => {
|
|
63
|
+
const query = {};
|
|
64
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
65
|
+
if (opts.page !== void 0) query.page = String(opts.page);
|
|
66
|
+
return sdk.client.fetch("/admin/faire/products", { query });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const badgeColor = (status) => {
|
|
70
|
+
switch (status) {
|
|
71
|
+
case "synced":
|
|
72
|
+
case "success":
|
|
73
|
+
return "green";
|
|
74
|
+
case "failed":
|
|
75
|
+
return "red";
|
|
76
|
+
case "draft":
|
|
77
|
+
case "pending":
|
|
78
|
+
return "orange";
|
|
79
|
+
default:
|
|
80
|
+
return "grey";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const FaireProductWidget = ({ data }) => {
|
|
84
|
+
var _a, _b, _c;
|
|
85
|
+
const queryClient = reactQuery.useQueryClient();
|
|
86
|
+
const productKey = ["faire", "product-status", data.id];
|
|
87
|
+
const statusQuery = reactQuery.useQuery({
|
|
88
|
+
queryKey: ["faire", "status"],
|
|
89
|
+
queryFn: () => faireApi.status()
|
|
90
|
+
});
|
|
91
|
+
const connected = !!((_a = statusQuery.data) == null ? void 0 : _a.connected);
|
|
92
|
+
const readiness = (_b = statusQuery.data) == null ? void 0 : _b.readiness;
|
|
93
|
+
const productStatusQuery = reactQuery.useQuery({
|
|
94
|
+
queryKey: productKey,
|
|
95
|
+
queryFn: () => faireApi.productStatus(data.id),
|
|
96
|
+
refetchInterval: (query) => {
|
|
97
|
+
var _a2, _b2;
|
|
98
|
+
const s = (_b2 = (_a2 = query.state.data) == null ? void 0 : _a2.latest) == null ? void 0 : _b2.status;
|
|
99
|
+
return s === "draft" || s === "pending" ? 1e4 : false;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
const latest = (_c = productStatusQuery.data) == null ? void 0 : _c.latest;
|
|
103
|
+
const syncMutation = reactQuery.useMutation({
|
|
104
|
+
mutationFn: () => faireApi.syncProduct(data.id),
|
|
105
|
+
onSuccess: () => {
|
|
106
|
+
queryClient.invalidateQueries({ queryKey: productKey });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const result = syncMutation.data ? syncMutation.data.result : null;
|
|
110
|
+
const error = syncMutation.error ? syncMutation.error.message || "Sync failed" : null;
|
|
111
|
+
const view = result ? {
|
|
112
|
+
published: result.published,
|
|
113
|
+
state: result.state,
|
|
114
|
+
product_url: result.product_url,
|
|
115
|
+
product_token: result.product_token,
|
|
116
|
+
warnings: result.warnings,
|
|
117
|
+
status: result.published ? "success" : result.state
|
|
118
|
+
} : latest ? {
|
|
119
|
+
published: latest.published,
|
|
120
|
+
state: latest.product_state,
|
|
121
|
+
product_url: latest.product_url,
|
|
122
|
+
product_token: latest.product_token,
|
|
123
|
+
warnings: latest.error_message ? String(latest.error_message).split(" | ") : void 0,
|
|
124
|
+
status: latest.status
|
|
125
|
+
} : null;
|
|
126
|
+
const notReady = connected && readiness && !readiness.ready_to_publish;
|
|
127
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
128
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
129
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Faire Sync" }),
|
|
131
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Sync this product to your Faire brand." })
|
|
132
|
+
] }),
|
|
133
|
+
view && /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: badgeColor(view.status), children: view.published ? "Active" : view.state || view.status })
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 px-6 py-4", children: [
|
|
136
|
+
!connected && !statusQuery.isLoading && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Faire is not connected. Connect it under Settings → Faire." }) }),
|
|
137
|
+
notReady && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "warning", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Publish readiness incomplete (brand or wholesale pricing config is missing). Syncing will still run and create the product as a draft." }) }),
|
|
138
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: error }) }),
|
|
139
|
+
view && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: view.published ? "success" : "warning", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
140
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: view.published ? "Published to Faire as active." : `Synced as ${view.state || view.status}.` }),
|
|
141
|
+
view.warnings && view.warnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: view.warnings.join(" | ") }),
|
|
142
|
+
view.product_url && /* @__PURE__ */ jsxRuntime.jsx(
|
|
143
|
+
"a",
|
|
144
|
+
{
|
|
145
|
+
href: view.product_url,
|
|
146
|
+
target: "_blank",
|
|
147
|
+
rel: "noreferrer",
|
|
148
|
+
className: "text-ui-fg-interactive hover:text-ui-fg-interactive-hover",
|
|
149
|
+
children: view.published ? "View on Faire →" : "Open in Faire →"
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
] }) }),
|
|
153
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
154
|
+
ui.Tooltip,
|
|
155
|
+
{
|
|
156
|
+
content: connected ? void 0 : "Connect Faire under Settings → Faire first.",
|
|
157
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
158
|
+
ui.Button,
|
|
159
|
+
{
|
|
160
|
+
size: "small",
|
|
161
|
+
isLoading: syncMutation.isPending,
|
|
162
|
+
disabled: !connected,
|
|
163
|
+
onClick: () => syncMutation.mutate(),
|
|
164
|
+
children: view ? "Re-sync to Faire" : "Sync to Faire"
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
) })
|
|
169
|
+
] })
|
|
170
|
+
] });
|
|
171
|
+
};
|
|
172
|
+
adminSdk.defineWidgetConfig({
|
|
173
|
+
zone: "product.details.side.before"
|
|
174
|
+
});
|
|
175
|
+
const columnHelper = ui.createDataTableColumnHelper();
|
|
176
|
+
const useFaireSyncColumns = () => {
|
|
177
|
+
return [
|
|
178
|
+
columnHelper.accessor("product_id", {
|
|
179
|
+
header: "Product",
|
|
180
|
+
cell: (info) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-ui-fg-subtle text-xs", children: info.getValue() })
|
|
181
|
+
}),
|
|
182
|
+
columnHelper.accessor("status", {
|
|
183
|
+
header: "Status",
|
|
184
|
+
cell: (info) => {
|
|
185
|
+
const status = info.getValue();
|
|
186
|
+
const color = status === "success" ? "green" : status === "failed" ? "red" : status === "draft" ? "orange" : "grey";
|
|
187
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color, children: status });
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
columnHelper.accessor("product_state", {
|
|
191
|
+
header: "State",
|
|
192
|
+
cell: (info) => info.getValue() || "—"
|
|
193
|
+
}),
|
|
194
|
+
columnHelper.display({
|
|
195
|
+
id: "product",
|
|
196
|
+
header: "Faire product",
|
|
197
|
+
cell: ({ row }) => {
|
|
198
|
+
const r = row.original;
|
|
199
|
+
if (!r.product_url) return "—";
|
|
200
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
201
|
+
"a",
|
|
202
|
+
{
|
|
203
|
+
href: r.product_url,
|
|
204
|
+
target: "_blank",
|
|
205
|
+
rel: "noreferrer",
|
|
206
|
+
onClick: (e) => e.stopPropagation(),
|
|
207
|
+
className: "text-ui-fg-interactive hover:text-ui-fg-interactive-hover",
|
|
208
|
+
children: r.product_token
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}),
|
|
213
|
+
columnHelper.accessor("synced_at", {
|
|
214
|
+
header: "Synced",
|
|
215
|
+
cell: (info) => {
|
|
216
|
+
const v = info.getValue();
|
|
217
|
+
return v ? new Date(v).toLocaleString() : "—";
|
|
218
|
+
},
|
|
219
|
+
enableSorting: true,
|
|
220
|
+
sortLabel: "Synced",
|
|
221
|
+
sortAscLabel: "Oldest first",
|
|
222
|
+
sortDescLabel: "Newest first"
|
|
223
|
+
})
|
|
224
|
+
];
|
|
225
|
+
};
|
|
226
|
+
const PAGE_SIZE = 10;
|
|
227
|
+
const STATUS_KEY$1 = ["faire", "status"];
|
|
228
|
+
const FaireSettingsPage = () => {
|
|
229
|
+
var _a, _b, _c;
|
|
230
|
+
const navigate = reactRouterDom.useNavigate();
|
|
231
|
+
const queryClient = reactQuery.useQueryClient();
|
|
232
|
+
const [pagination, setPagination] = react.useState({
|
|
233
|
+
pageIndex: 0,
|
|
234
|
+
pageSize: PAGE_SIZE
|
|
235
|
+
});
|
|
236
|
+
const [filtering, setFiltering] = react.useState({});
|
|
237
|
+
const statusQuery = reactQuery.useQuery({
|
|
238
|
+
queryKey: STATUS_KEY$1,
|
|
239
|
+
queryFn: () => faireApi.status()
|
|
240
|
+
});
|
|
241
|
+
const status = statusQuery.data;
|
|
242
|
+
const connected = !!(status == null ? void 0 : status.connected);
|
|
243
|
+
const syncsQuery = reactQuery.useQuery({
|
|
244
|
+
queryKey: ["faire", "syncs", pagination, filtering],
|
|
245
|
+
placeholderData: reactQuery.keepPreviousData,
|
|
246
|
+
queryFn: async () => {
|
|
247
|
+
var _a2;
|
|
248
|
+
const statusFilter = (_a2 = filtering.status) == null ? void 0 : _a2[0];
|
|
249
|
+
const res = await faireApi.listSyncs({
|
|
250
|
+
take: pagination.pageSize,
|
|
251
|
+
skip: pagination.pageIndex * pagination.pageSize,
|
|
252
|
+
status: statusFilter
|
|
253
|
+
});
|
|
254
|
+
return { syncs: res.syncs || [], count: res.count || 0 };
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const connectMutation = reactQuery.useMutation({
|
|
258
|
+
mutationFn: () => faireApi.authorize(),
|
|
259
|
+
onSuccess: (res) => {
|
|
260
|
+
window.open(res.authorization_url, "_blank");
|
|
261
|
+
ui.toast.info("Faire authorization opened", {
|
|
262
|
+
description: "Complete the authorization in the new tab."
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
onError: (err) => ui.toast.error("Failed to start Faire authorization", {
|
|
266
|
+
description: err.message
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
const disconnectMutation = reactQuery.useMutation({
|
|
270
|
+
mutationFn: () => faireApi.disconnect(),
|
|
271
|
+
onSuccess: () => {
|
|
272
|
+
queryClient.invalidateQueries({ queryKey: STATUS_KEY$1 });
|
|
273
|
+
ui.toast.success("Disconnected from Faire");
|
|
274
|
+
},
|
|
275
|
+
onError: (err) => ui.toast.error("Failed to disconnect", { description: err.message })
|
|
276
|
+
});
|
|
277
|
+
const columns = useFaireSyncColumns();
|
|
278
|
+
const filterHelper = ui.createDataTableFilterHelper();
|
|
279
|
+
const filters = [
|
|
280
|
+
filterHelper.accessor("status", {
|
|
281
|
+
type: "select",
|
|
282
|
+
label: "Status",
|
|
283
|
+
options: [
|
|
284
|
+
{ label: "Success", value: "success" },
|
|
285
|
+
{ label: "Draft", value: "draft" },
|
|
286
|
+
{ label: "Failed", value: "failed" },
|
|
287
|
+
{ label: "Pending", value: "pending" },
|
|
288
|
+
{ label: "Syncing", value: "syncing" }
|
|
289
|
+
]
|
|
290
|
+
})
|
|
291
|
+
];
|
|
292
|
+
const table = ui.useDataTable({
|
|
293
|
+
data: ((_a = syncsQuery.data) == null ? void 0 : _a.syncs) ?? [],
|
|
294
|
+
columns,
|
|
295
|
+
rowCount: ((_b = syncsQuery.data) == null ? void 0 : _b.count) ?? 0,
|
|
296
|
+
getRowId: (row) => row.id,
|
|
297
|
+
onRowClick: (_, row) => {
|
|
298
|
+
navigate(`/settings/faire/${row.id}`);
|
|
299
|
+
},
|
|
300
|
+
isLoading: syncsQuery.isLoading,
|
|
301
|
+
filters,
|
|
302
|
+
pagination: {
|
|
303
|
+
state: pagination,
|
|
304
|
+
onPaginationChange: setPagination
|
|
305
|
+
},
|
|
306
|
+
filtering: {
|
|
307
|
+
state: filtering,
|
|
308
|
+
onFilteringChange: setFiltering
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
if (statusQuery.isLoading) {
|
|
312
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
313
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
314
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
315
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-6 w-32" }),
|
|
316
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-7 w-28" })
|
|
317
|
+
] }),
|
|
318
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-4 px-6 py-4", children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }, i)) })
|
|
319
|
+
] }),
|
|
320
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-0", children: [
|
|
321
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-5 w-32" }) }),
|
|
322
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3 px-6 pb-6", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }, i)) })
|
|
323
|
+
] })
|
|
324
|
+
] });
|
|
325
|
+
}
|
|
326
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
327
|
+
statusQuery.isError && /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: ((_c = statusQuery.error) == null ? void 0 : _c.message) || "Failed to load Faire status" }),
|
|
328
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
329
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
330
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
331
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Faire Sync" }),
|
|
332
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", children: "Connect your Faire brand and sync Medusa products, inventory and orders bidirectionally." })
|
|
333
|
+
] }),
|
|
334
|
+
connected ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
335
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: "green", children: "Connected" }),
|
|
336
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
337
|
+
ui.Button,
|
|
338
|
+
{
|
|
339
|
+
size: "small",
|
|
340
|
+
variant: "secondary",
|
|
341
|
+
onClick: () => navigate("/settings/faire/settings"),
|
|
342
|
+
children: "Sync settings"
|
|
343
|
+
}
|
|
344
|
+
),
|
|
345
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
346
|
+
ui.Button,
|
|
347
|
+
{
|
|
348
|
+
size: "small",
|
|
349
|
+
variant: "danger",
|
|
350
|
+
onClick: () => disconnectMutation.mutate(),
|
|
351
|
+
isLoading: disconnectMutation.isPending,
|
|
352
|
+
children: "Disconnect"
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
356
|
+
ui.Button,
|
|
357
|
+
{
|
|
358
|
+
size: "small",
|
|
359
|
+
onClick: () => connectMutation.mutate(),
|
|
360
|
+
isLoading: connectMutation.isPending,
|
|
361
|
+
children: "Connect Faire"
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
] }),
|
|
365
|
+
connected && (status == null ? void 0 : status.account) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 grid grid-cols-4 gap-4", children: [
|
|
366
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoField, { label: "Brand", value: status.account.brand_name }),
|
|
367
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoField, { label: "Brand ID", value: status.account.brand_id }),
|
|
368
|
+
/* @__PURE__ */ jsxRuntime.jsx(InfoField, { label: "Currency", value: status.account.currency || "—" }),
|
|
369
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
370
|
+
InfoField,
|
|
371
|
+
{
|
|
372
|
+
label: "Token expires",
|
|
373
|
+
value: status.account.token_expires_at ? new Date(status.account.token_expires_at).toLocaleString() : "Does not expire"
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
] })
|
|
377
|
+
] }),
|
|
378
|
+
/* @__PURE__ */ jsxRuntime.jsx(BulkOperations, { connected }),
|
|
379
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y p-0", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.DataTable, { instance: table, children: [
|
|
380
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.DataTable.Toolbar, { className: "flex flex-col md:flex-row justify-between gap-y-4 px-6 py-4", children: [
|
|
381
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
382
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Recent syncs" }),
|
|
383
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: "Latest product sync attempts. Click a row for details." })
|
|
384
|
+
] }),
|
|
385
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-x-2", children: [
|
|
386
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.FilterMenu, { tooltip: "Filter syncs" }),
|
|
387
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
388
|
+
ui.Button,
|
|
389
|
+
{
|
|
390
|
+
size: "small",
|
|
391
|
+
variant: "secondary",
|
|
392
|
+
onClick: () => syncsQuery.refetch(),
|
|
393
|
+
isLoading: syncsQuery.isFetching,
|
|
394
|
+
children: "Refresh"
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
] })
|
|
398
|
+
] }),
|
|
399
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.Table, {}),
|
|
400
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.Pagination, {})
|
|
401
|
+
] }) }),
|
|
402
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {})
|
|
403
|
+
] });
|
|
404
|
+
};
|
|
405
|
+
const InfoField = ({ label, value }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
406
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
|
|
407
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: value })
|
|
408
|
+
] });
|
|
409
|
+
const BulkOperations = ({ connected }) => {
|
|
410
|
+
var _a, _b, _c, _d, _e, _f;
|
|
411
|
+
const [productIds, setProductIds] = react.useState("");
|
|
412
|
+
const [pushBatch, setPushBatch] = react.useState(null);
|
|
413
|
+
const [pullBatch, setPullBatch] = react.useState(null);
|
|
414
|
+
const pushMutation = reactQuery.useMutation({
|
|
415
|
+
mutationFn: (ids) => faireApi.syncBulk(ids),
|
|
416
|
+
onSuccess: (res) => {
|
|
417
|
+
setPushBatch(res.batch_id);
|
|
418
|
+
ui.toast.success("Bulk product sync started", {
|
|
419
|
+
description: "Running in the background. Polling progress…"
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
onError: (err) => ui.toast.error("Failed to start bulk sync", { description: err.message })
|
|
423
|
+
});
|
|
424
|
+
const pullMutation = reactQuery.useMutation({
|
|
425
|
+
mutationFn: () => faireApi.ingestOrders(),
|
|
426
|
+
onSuccess: (res) => {
|
|
427
|
+
setPullBatch(res.batch_id);
|
|
428
|
+
ui.toast.success("Faire order pull started", {
|
|
429
|
+
description: "Running in the background. Polling progress…"
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
onError: (err) => ui.toast.error("Failed to start order pull", { description: err.message })
|
|
433
|
+
});
|
|
434
|
+
const pushStatus = reactQuery.useQuery({
|
|
435
|
+
queryKey: ["faire", "bulk", pushBatch],
|
|
436
|
+
enabled: !!pushBatch,
|
|
437
|
+
refetchInterval: (q) => {
|
|
438
|
+
var _a2, _b2;
|
|
439
|
+
return ((_b2 = (_a2 = q.state.data) == null ? void 0 : _a2.progress) == null ? void 0 : _b2.finished) ? false : 3e3;
|
|
440
|
+
},
|
|
441
|
+
queryFn: () => faireApi.bulkStatus(pushBatch)
|
|
442
|
+
});
|
|
443
|
+
const pullStatus = reactQuery.useQuery({
|
|
444
|
+
queryKey: ["faire", "ingest", pullBatch],
|
|
445
|
+
enabled: !!pullBatch,
|
|
446
|
+
refetchInterval: (q) => {
|
|
447
|
+
var _a2, _b2;
|
|
448
|
+
return ((_b2 = (_a2 = q.state.data) == null ? void 0 : _a2.progress) == null ? void 0 : _b2.finished) ? false : 3e3;
|
|
449
|
+
},
|
|
450
|
+
queryFn: () => faireApi.ingestStatus(pullBatch)
|
|
451
|
+
});
|
|
452
|
+
const pushProgress = (_a = pushStatus.data) == null ? void 0 : _a.progress;
|
|
453
|
+
const pullProgress = (_b = pullStatus.data) == null ? void 0 : _b.progress;
|
|
454
|
+
const handlePush = () => {
|
|
455
|
+
const ids = productIds.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
|
|
456
|
+
if (!ids.length) {
|
|
457
|
+
ui.toast.error("Enter at least one product id");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
pushMutation.mutate(ids);
|
|
461
|
+
};
|
|
462
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
463
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [
|
|
464
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: "Bulk operations" }),
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: "Push many products to Faire, or pull wholesale orders from Faire into Medusa. Both run as long-running background workflows." })
|
|
466
|
+
] }),
|
|
467
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 flex flex-col gap-6", children: [
|
|
468
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
469
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
470
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Push products to Faire" }),
|
|
471
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: "Comma- or line-separated Medusa product IDs." })
|
|
472
|
+
] }),
|
|
473
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
474
|
+
"textarea",
|
|
475
|
+
{
|
|
476
|
+
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm min-h-[72px]",
|
|
477
|
+
placeholder: "prod_01..., prod_02...",
|
|
478
|
+
value: productIds,
|
|
479
|
+
onChange: (e) => setProductIds(e.target.value),
|
|
480
|
+
disabled: !connected || pushMutation.isPending
|
|
481
|
+
}
|
|
482
|
+
),
|
|
483
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
484
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
485
|
+
ui.Button,
|
|
486
|
+
{
|
|
487
|
+
size: "small",
|
|
488
|
+
onClick: handlePush,
|
|
489
|
+
disabled: !connected || pushMutation.isPending,
|
|
490
|
+
isLoading: pushMutation.isPending,
|
|
491
|
+
children: "Start bulk push"
|
|
492
|
+
}
|
|
493
|
+
),
|
|
494
|
+
pushBatch && pushProgress && /* @__PURE__ */ jsxRuntime.jsx(
|
|
495
|
+
BulkProgress,
|
|
496
|
+
{
|
|
497
|
+
label: "Push",
|
|
498
|
+
progress: pushProgress,
|
|
499
|
+
status: (_d = (_c = pushStatus.data) == null ? void 0 : _c.batch) == null ? void 0 : _d.status
|
|
500
|
+
}
|
|
501
|
+
)
|
|
502
|
+
] })
|
|
503
|
+
] }),
|
|
504
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
505
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
506
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: "Pull Faire orders into Medusa" }),
|
|
507
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: "Ingests all available Faire orders as Medusa orders (idempotent)." })
|
|
508
|
+
] }),
|
|
509
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
510
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
511
|
+
ui.Button,
|
|
512
|
+
{
|
|
513
|
+
size: "small",
|
|
514
|
+
variant: "secondary",
|
|
515
|
+
onClick: () => pullMutation.mutate(),
|
|
516
|
+
disabled: !connected || pullMutation.isPending,
|
|
517
|
+
isLoading: pullMutation.isPending,
|
|
518
|
+
children: "Start order pull"
|
|
519
|
+
}
|
|
520
|
+
),
|
|
521
|
+
pullBatch && pullProgress && /* @__PURE__ */ jsxRuntime.jsx(
|
|
522
|
+
BulkProgress,
|
|
523
|
+
{
|
|
524
|
+
label: "Pull",
|
|
525
|
+
progress: pullProgress,
|
|
526
|
+
status: (_f = (_e = pullStatus.data) == null ? void 0 : _e.batch) == null ? void 0 : _f.status
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
] })
|
|
530
|
+
] })
|
|
531
|
+
] })
|
|
532
|
+
] });
|
|
533
|
+
};
|
|
534
|
+
const BulkProgress = ({
|
|
535
|
+
label,
|
|
536
|
+
progress,
|
|
537
|
+
status
|
|
538
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
539
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.StatusBadge, { color: progress.finished ? status === "failed" ? "red" : "green" : "orange", children: [
|
|
540
|
+
label,
|
|
541
|
+
": ",
|
|
542
|
+
progress.done,
|
|
543
|
+
"/",
|
|
544
|
+
progress.total || "?",
|
|
545
|
+
" (",
|
|
546
|
+
progress.pct,
|
|
547
|
+
"%)"
|
|
548
|
+
] }),
|
|
549
|
+
progress.finished && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: status === "failed" ? "Completed with errors" : "Done" })
|
|
550
|
+
] });
|
|
551
|
+
const config = adminSdk.defineRouteConfig({
|
|
552
|
+
label: "Faire",
|
|
553
|
+
icon: icons.BuildingStorefront
|
|
554
|
+
});
|
|
555
|
+
const handle$2 = {
|
|
556
|
+
breadcrumb: () => "Faire Sync"
|
|
557
|
+
};
|
|
558
|
+
const STATUS_COLORS = {
|
|
559
|
+
success: "green",
|
|
560
|
+
failed: "red",
|
|
561
|
+
draft: "orange",
|
|
562
|
+
pending: "grey",
|
|
563
|
+
syncing: "grey"
|
|
564
|
+
};
|
|
565
|
+
const FaireSyncDetailPage = () => {
|
|
566
|
+
const { id } = reactRouterDom.useParams();
|
|
567
|
+
const navigate = reactRouterDom.useNavigate();
|
|
568
|
+
const [record, setRecord] = react.useState(null);
|
|
569
|
+
const [loading, setLoading] = react.useState(true);
|
|
570
|
+
const [retrying, setRetrying] = react.useState(false);
|
|
571
|
+
const [error, setError] = react.useState(null);
|
|
572
|
+
react.useEffect(() => {
|
|
573
|
+
if (!id) return;
|
|
574
|
+
setLoading(true);
|
|
575
|
+
faireApi.getSync(id).then((res) => {
|
|
576
|
+
setRecord(res.sync || res);
|
|
577
|
+
}).catch((err) => setError(err.message || "Failed to load sync record")).finally(() => setLoading(false));
|
|
578
|
+
}, [id]);
|
|
579
|
+
const handleRetry = async () => {
|
|
580
|
+
if (!record) return;
|
|
581
|
+
setRetrying(true);
|
|
582
|
+
setError(null);
|
|
583
|
+
try {
|
|
584
|
+
await faireApi.retrySync(record.id);
|
|
585
|
+
const updated = await faireApi.getSync(record.id).catch(() => null);
|
|
586
|
+
if (updated) setRecord(updated.sync || updated);
|
|
587
|
+
} catch (err) {
|
|
588
|
+
setError(err.message || "Retry failed");
|
|
589
|
+
} finally {
|
|
590
|
+
setRetrying(false);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
if (loading) {
|
|
594
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-7 w-12 rounded-md" }) });
|
|
595
|
+
}
|
|
596
|
+
if (!record) {
|
|
597
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "p-6 flex flex-col gap-4", children: [
|
|
598
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Sync record not found" }),
|
|
599
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { size: "small", variant: "secondary", onClick: () => navigate("/settings/faire"), children: "Back to Faire settings" })
|
|
600
|
+
] });
|
|
601
|
+
}
|
|
602
|
+
const warnings = record.error_message ? record.error_message.split(" | ") : [];
|
|
603
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
604
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-tag-red-text", children: error }) }),
|
|
605
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
606
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4", children: [
|
|
607
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
608
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Sync record" }),
|
|
609
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle font-mono text-xs", children: record.id })
|
|
610
|
+
] }),
|
|
611
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
612
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: STATUS_COLORS[record.status] || "grey", children: record.status }),
|
|
613
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu, { children: [
|
|
614
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(ui.IconButton, { size: "small", variant: "transparent", disabled: retrying, children: /* @__PURE__ */ jsxRuntime.jsx(icons.EllipsisHorizontal, {}) }) }),
|
|
615
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.DropdownMenu.Content, { align: "end", children: [
|
|
616
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
617
|
+
ui.DropdownMenu.Item,
|
|
618
|
+
{
|
|
619
|
+
className: "gap-x-2",
|
|
620
|
+
onClick: handleRetry,
|
|
621
|
+
disabled: retrying,
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ArrowPath, { className: "text-ui-fg-subtle" }),
|
|
624
|
+
retrying ? "Re-syncing…" : "Re-sync product"
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
),
|
|
628
|
+
record.product_url && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
629
|
+
ui.DropdownMenu.Item,
|
|
630
|
+
{
|
|
631
|
+
className: "gap-x-2",
|
|
632
|
+
onClick: () => window.open(record.product_url, "_blank"),
|
|
633
|
+
children: [
|
|
634
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "text-ui-fg-subtle" }),
|
|
635
|
+
"Open on Faire"
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
)
|
|
639
|
+
] })
|
|
640
|
+
] })
|
|
641
|
+
] })
|
|
642
|
+
] }),
|
|
643
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-4 grid grid-cols-2 gap-4", children: [
|
|
644
|
+
/* @__PURE__ */ jsxRuntime.jsx(Detail, { label: "Product", value: record.product_id, mono: true }),
|
|
645
|
+
/* @__PURE__ */ jsxRuntime.jsx(Detail, { label: "Action", value: record.action }),
|
|
646
|
+
/* @__PURE__ */ jsxRuntime.jsx(Detail, { label: "Faire product token", value: record.product_token || "—", mono: true }),
|
|
647
|
+
/* @__PURE__ */ jsxRuntime.jsx(Detail, { label: "State", value: record.product_state || "—" }),
|
|
648
|
+
/* @__PURE__ */ jsxRuntime.jsx(Detail, { label: "Published", value: record.published ? "Yes" : "No" }),
|
|
649
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
650
|
+
Detail,
|
|
651
|
+
{
|
|
652
|
+
label: "Synced at",
|
|
653
|
+
value: record.synced_at ? new Date(record.synced_at).toLocaleString() : "—"
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
] })
|
|
657
|
+
] }),
|
|
658
|
+
warnings.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "divide-y p-0", children: [
|
|
659
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Issues" }) }),
|
|
660
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 py-4 flex flex-col gap-2", children: warnings.map((w, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-tag-red-text text-sm", children: w }, i)) })
|
|
661
|
+
] }),
|
|
662
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {})
|
|
663
|
+
] });
|
|
664
|
+
};
|
|
665
|
+
const Detail = ({
|
|
666
|
+
label,
|
|
667
|
+
value,
|
|
668
|
+
mono
|
|
669
|
+
}) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
670
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
|
|
671
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: mono ? "font-mono text-xs" : "", children: value })
|
|
672
|
+
] });
|
|
673
|
+
const handle$1 = {
|
|
674
|
+
breadcrumb: () => "Sync record"
|
|
675
|
+
};
|
|
676
|
+
const STATUS_KEY = ["faire", "status"];
|
|
677
|
+
const PARENT = "/settings/faire";
|
|
678
|
+
const NONE = "__none__";
|
|
679
|
+
const cleanSettings = (form) => {
|
|
680
|
+
const out = { ...form };
|
|
681
|
+
for (const key of [
|
|
682
|
+
"default_brand_id",
|
|
683
|
+
"default_shipping_policy_id",
|
|
684
|
+
"default_category"
|
|
685
|
+
]) {
|
|
686
|
+
if (!out[key] || out[key] === NONE) out[key] = null;
|
|
687
|
+
}
|
|
688
|
+
if (out.default_wholesale_markup_percent === "" || out.default_wholesale_markup_percent === NONE) {
|
|
689
|
+
out.default_wholesale_markup_percent = null;
|
|
690
|
+
}
|
|
691
|
+
if (out.default_lead_time_days === "" || out.default_lead_time_days === NONE) {
|
|
692
|
+
out.default_lead_time_days = null;
|
|
693
|
+
}
|
|
694
|
+
return out;
|
|
695
|
+
};
|
|
696
|
+
const FaireSyncSettingsDrawer = () => {
|
|
697
|
+
const navigate = reactRouterDom.useNavigate();
|
|
698
|
+
const queryClient = reactQuery.useQueryClient();
|
|
699
|
+
const [open, setOpen] = react.useState(true);
|
|
700
|
+
const [form, setForm] = react.useState({});
|
|
701
|
+
const close = () => {
|
|
702
|
+
setOpen(false);
|
|
703
|
+
navigate(PARENT);
|
|
704
|
+
};
|
|
705
|
+
const statusQuery = reactQuery.useQuery({
|
|
706
|
+
queryKey: STATUS_KEY,
|
|
707
|
+
queryFn: () => faireApi.status()
|
|
708
|
+
});
|
|
709
|
+
const status = statusQuery.data;
|
|
710
|
+
const connected = !!(status == null ? void 0 : status.connected);
|
|
711
|
+
react.useEffect(() => {
|
|
712
|
+
if (status == null ? void 0 : status.settings) setForm(status.settings);
|
|
713
|
+
}, [status == null ? void 0 : status.settings]);
|
|
714
|
+
const saveMutation = reactQuery.useMutation({
|
|
715
|
+
mutationFn: (payload) => faireApi.saveSettings(payload),
|
|
716
|
+
onSuccess: () => {
|
|
717
|
+
queryClient.invalidateQueries({ queryKey: STATUS_KEY });
|
|
718
|
+
ui.toast.success("Sync settings saved");
|
|
719
|
+
close();
|
|
720
|
+
},
|
|
721
|
+
onError: (err) => ui.toast.error("Failed to save settings", { description: err.message })
|
|
722
|
+
});
|
|
723
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer, { open, onOpenChange: (o) => o ? setOpen(true) : close(), children: [
|
|
724
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
725
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
|
|
726
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Title, { children: "Sync settings" }),
|
|
727
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { children: "Publish readiness and the defaults applied to every product pushed to Faire." })
|
|
728
|
+
] }),
|
|
729
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Body, { className: "flex flex-col gap-6 overflow-y-auto", children: statusQuery.isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(ui.Skeleton, { className: "h-10 w-full" }, i)) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
730
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
731
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
732
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Publish readiness" }),
|
|
733
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.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." })
|
|
734
|
+
] }),
|
|
735
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
736
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.connected, label: "Faire connected" }),
|
|
737
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.brand, label: "Brand configured" }),
|
|
738
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.wholesale_pricing, label: "Wholesale pricing" }),
|
|
739
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChecklistItem, { ok: status == null ? void 0 : status.readiness.shipping_policy, label: "Shipping policy" })
|
|
740
|
+
] })
|
|
741
|
+
] }),
|
|
742
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-3", children: [
|
|
743
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
744
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Sync defaults" }),
|
|
745
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-ui-fg-subtle", size: "small", children: "Applied to every product unless overridden in product metadata." })
|
|
746
|
+
] }),
|
|
747
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-4", children: [
|
|
748
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Brand ID", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
749
|
+
"input",
|
|
750
|
+
{
|
|
751
|
+
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
752
|
+
value: String(form.default_brand_id ?? ""),
|
|
753
|
+
onChange: (e) => setForm({ ...form, default_brand_id: e.target.value || null }),
|
|
754
|
+
disabled: !connected,
|
|
755
|
+
placeholder: "b_..."
|
|
756
|
+
}
|
|
757
|
+
) }),
|
|
758
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Wholesale markup % (off retail)", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: "Wholesale price = retail × (100 − markup%) / 100. E.g. 50 → half of retail.", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
759
|
+
"input",
|
|
760
|
+
{
|
|
761
|
+
type: "number",
|
|
762
|
+
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
763
|
+
value: String(form.default_wholesale_markup_percent ?? ""),
|
|
764
|
+
onChange: (e) => setForm({
|
|
765
|
+
...form,
|
|
766
|
+
default_wholesale_markup_percent: e.target.value ? Number(e.target.value) : null
|
|
767
|
+
}),
|
|
768
|
+
disabled: !connected,
|
|
769
|
+
placeholder: "e.g. 50"
|
|
770
|
+
}
|
|
771
|
+
) }) }),
|
|
772
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Default min order quantity", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
773
|
+
"input",
|
|
774
|
+
{
|
|
775
|
+
type: "number",
|
|
776
|
+
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
777
|
+
value: String(form.default_min_order_quantity ?? 1),
|
|
778
|
+
onChange: (e) => setForm({
|
|
779
|
+
...form,
|
|
780
|
+
default_min_order_quantity: e.target.value ? Number(e.target.value) : 1
|
|
781
|
+
}),
|
|
782
|
+
disabled: !connected
|
|
783
|
+
}
|
|
784
|
+
) }),
|
|
785
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Default lead time (days)", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
786
|
+
"input",
|
|
787
|
+
{
|
|
788
|
+
type: "number",
|
|
789
|
+
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
790
|
+
value: String(form.default_lead_time_days ?? ""),
|
|
791
|
+
onChange: (e) => setForm({
|
|
792
|
+
...form,
|
|
793
|
+
default_lead_time_days: e.target.value ? Number(e.target.value) : null
|
|
794
|
+
}),
|
|
795
|
+
disabled: !connected,
|
|
796
|
+
placeholder: "e.g. 14"
|
|
797
|
+
}
|
|
798
|
+
) }),
|
|
799
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Default category", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
800
|
+
"input",
|
|
801
|
+
{
|
|
802
|
+
className: "border-ui-border-base rounded-lg border px-3 py-2 text-sm",
|
|
803
|
+
value: String(form.default_category ?? ""),
|
|
804
|
+
onChange: (e) => setForm({ ...form, default_category: e.target.value || null }),
|
|
805
|
+
disabled: !connected,
|
|
806
|
+
placeholder: "e.g. Home Decor"
|
|
807
|
+
}
|
|
808
|
+
) }),
|
|
809
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Follow product status", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: "If on, published Medusa products are published on Faire; draft products sync as drafts.", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
810
|
+
ui.Switch,
|
|
811
|
+
{
|
|
812
|
+
checked: form.follow_product_status !== false,
|
|
813
|
+
onCheckedChange: (v) => setForm({ ...form, follow_product_status: v })
|
|
814
|
+
}
|
|
815
|
+
) }) }),
|
|
816
|
+
/* @__PURE__ */ jsxRuntime.jsx(Field, { label: "Auto publish", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: "Publish products to Faire automatically on sync (otherwise create as draft).", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
817
|
+
ui.Switch,
|
|
818
|
+
{
|
|
819
|
+
checked: !!form.auto_publish,
|
|
820
|
+
onCheckedChange: (v) => setForm({ ...form, auto_publish: v })
|
|
821
|
+
}
|
|
822
|
+
) }) })
|
|
823
|
+
] })
|
|
824
|
+
] })
|
|
825
|
+
] }) }),
|
|
826
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { children: [
|
|
827
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "secondary", onClick: close, children: "Cancel" }),
|
|
828
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
829
|
+
ui.Button,
|
|
830
|
+
{
|
|
831
|
+
onClick: () => saveMutation.mutate(cleanSettings(form)),
|
|
832
|
+
isLoading: saveMutation.isPending,
|
|
833
|
+
disabled: !connected,
|
|
834
|
+
children: "Save settings"
|
|
835
|
+
}
|
|
836
|
+
)
|
|
837
|
+
] })
|
|
838
|
+
] }),
|
|
839
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {})
|
|
840
|
+
] });
|
|
841
|
+
};
|
|
842
|
+
const Field = ({ label, children }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
843
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { children: label }),
|
|
844
|
+
children
|
|
845
|
+
] });
|
|
846
|
+
const ChecklistItem = ({ ok, label }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
847
|
+
"div",
|
|
848
|
+
{
|
|
849
|
+
className: ui.clx(
|
|
850
|
+
"flex items-center gap-2 rounded-lg border px-3 py-2",
|
|
851
|
+
ok ? "border-ui-tag-green-border bg-ui-tag-green-bg" : "border-ui-tag-red-border bg-ui-tag-red-bg"
|
|
852
|
+
),
|
|
853
|
+
children: [
|
|
854
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: ok ? "green" : "red", children: ok ? "Ready" : "Missing" }),
|
|
855
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: label })
|
|
856
|
+
]
|
|
857
|
+
}
|
|
858
|
+
);
|
|
859
|
+
const handle = {
|
|
860
|
+
breadcrumb: () => "Sync settings"
|
|
861
|
+
};
|
|
862
|
+
const FaireOauthCallback = () => {
|
|
863
|
+
const [params] = reactRouterDom.useSearchParams();
|
|
864
|
+
const navigate = reactRouterDom.useNavigate();
|
|
865
|
+
const [error, setError] = react.useState(null);
|
|
866
|
+
const exchangedRef = react.useRef(false);
|
|
867
|
+
react.useEffect(() => {
|
|
868
|
+
if (exchangedRef.current) return;
|
|
869
|
+
exchangedRef.current = true;
|
|
870
|
+
const code = params.get("code");
|
|
871
|
+
const state = params.get("state");
|
|
872
|
+
const errParam = params.get("error");
|
|
873
|
+
if (errParam) {
|
|
874
|
+
setError(
|
|
875
|
+
`Faire authorization failed: ${params.get("error_description") || errParam}`
|
|
876
|
+
);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (!code || !state) {
|
|
880
|
+
setError("Missing code or state in Faire callback.");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
faireApi.callback(code, state).then(() => {
|
|
884
|
+
navigate("/settings/faire", { replace: true });
|
|
885
|
+
}).catch((err) => {
|
|
886
|
+
setError(err.message || "Failed to complete Faire authorization.");
|
|
887
|
+
});
|
|
888
|
+
}, [params, navigate]);
|
|
889
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { className: "flex flex-col items-center gap-4 p-8", children: [
|
|
890
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h1", children: "Connecting Faire…" }),
|
|
891
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: error }) }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Completing authorization, please wait…" }),
|
|
892
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(
|
|
893
|
+
ui.Button,
|
|
894
|
+
{
|
|
895
|
+
size: "small",
|
|
896
|
+
variant: "secondary",
|
|
897
|
+
onClick: () => navigate("/settings/faire", { replace: true }),
|
|
898
|
+
children: "Back to Faire settings"
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
] });
|
|
902
|
+
};
|
|
903
|
+
const widgetModule = { widgets: [
|
|
904
|
+
{
|
|
905
|
+
Component: FaireProductWidget,
|
|
906
|
+
zone: ["product.details.side.before"],
|
|
907
|
+
widgetId: "Widget-50e7"
|
|
908
|
+
}
|
|
909
|
+
] };
|
|
910
|
+
const routeModule = {
|
|
911
|
+
routes: [
|
|
912
|
+
{
|
|
913
|
+
Component: FaireSettingsPage,
|
|
914
|
+
path: "/settings/faire",
|
|
915
|
+
handle: handle$2
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
Component: FaireSyncDetailPage,
|
|
919
|
+
path: "/settings/faire/:id",
|
|
920
|
+
handle: handle$1
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
Component: FaireSyncSettingsDrawer,
|
|
924
|
+
path: "/settings/faire/settings",
|
|
925
|
+
handle
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
Component: FaireOauthCallback,
|
|
929
|
+
path: "/settings/oauth/faire/callback"
|
|
930
|
+
}
|
|
931
|
+
]
|
|
932
|
+
};
|
|
933
|
+
const menuItemModule = {
|
|
934
|
+
menuItems: [
|
|
935
|
+
{
|
|
936
|
+
label: config.label,
|
|
937
|
+
icon: config.icon,
|
|
938
|
+
path: "/settings/faire",
|
|
939
|
+
nested: void 0,
|
|
940
|
+
rank: void 0,
|
|
941
|
+
translationNs: void 0
|
|
942
|
+
}
|
|
943
|
+
]
|
|
944
|
+
};
|
|
945
|
+
const formModule = { customFields: {} };
|
|
946
|
+
const displayModule = {
|
|
947
|
+
displays: {}
|
|
948
|
+
};
|
|
949
|
+
const i18nModule = { resources: {} };
|
|
950
|
+
const layoutModule = { layouts: [] };
|
|
951
|
+
const plugin = {
|
|
952
|
+
widgetModule,
|
|
953
|
+
routeModule,
|
|
954
|
+
menuItemModule,
|
|
955
|
+
formModule,
|
|
956
|
+
displayModule,
|
|
957
|
+
i18nModule,
|
|
958
|
+
layoutModule
|
|
959
|
+
};
|
|
960
|
+
module.exports = plugin;
|