@lodashventure/medusa-review 1.4.14 → 1.4.15
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.
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
3
|
+
const adminSdk = require("@medusajs/admin-sdk");
|
|
4
|
+
const icons = require("@medusajs/icons");
|
|
5
|
+
const ui = require("@medusajs/ui");
|
|
6
|
+
const axios = require("axios");
|
|
7
|
+
const Mentions = require("rc-mentions");
|
|
8
|
+
const react = require("react");
|
|
9
|
+
const reactRouterDom = require("react-router-dom");
|
|
10
|
+
const Medusa = require("@medusajs/js-sdk");
|
|
11
|
+
require("@medusajs/admin-shared");
|
|
12
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
13
|
+
const axios__default = /* @__PURE__ */ _interopDefault(axios);
|
|
14
|
+
const Mentions__default = /* @__PURE__ */ _interopDefault(Mentions);
|
|
15
|
+
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
16
|
+
const statusColor = (status) => status === "approved" ? "green" : status === "rejected" ? "red" : "grey";
|
|
17
|
+
const mimeTypes = ["image", "video"];
|
|
18
|
+
const MediaViewer = ({ media, className }) => {
|
|
19
|
+
const [open, setOpen] = react.useState(false);
|
|
20
|
+
const mimeType = media.mimeType.split("/")[0];
|
|
21
|
+
if (!mimeTypes.includes(mimeType)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal, { open, onOpenChange: setOpen, children: [
|
|
25
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Trigger, { className: "bg-ui-bg-overlay ", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ui.clx("w-24 h-24", className), children: [
|
|
26
|
+
mimeType === "image" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-0 w-full h-full object-contain flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: media.fileUrl, alt: "an image" }) }),
|
|
27
|
+
mimeType === "video" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: "Watch Video" }) })
|
|
28
|
+
] }) }),
|
|
29
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal.Content, { className: "w-[80vw] h-[80vh] mx-auto my-auto", children: [
|
|
30
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal.Header, { children: [
|
|
31
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Title, { children: media.fileId }),
|
|
32
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.FocusModal.Close, {})
|
|
33
|
+
] }),
|
|
34
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.FocusModal.Body, { className: "flex items-center justify-center", children: [
|
|
35
|
+
mimeType === "image" && /* @__PURE__ */ jsxRuntime.jsx("img", { src: media.fileUrl, alt: "an image" }),
|
|
36
|
+
mimeType === "video" && /* @__PURE__ */ jsxRuntime.jsx("video", { src: media.fileUrl, controls: true })
|
|
37
|
+
] })
|
|
38
|
+
] })
|
|
39
|
+
] }) });
|
|
40
|
+
};
|
|
41
|
+
const statusMap = {
|
|
42
|
+
pending: "in-progress",
|
|
43
|
+
approved: "completed",
|
|
44
|
+
rejected: "completed"
|
|
45
|
+
};
|
|
46
|
+
const ReviewChild = ({ review, onAction }) => {
|
|
47
|
+
var _a, _b, _c;
|
|
48
|
+
const color = statusColor(review.status);
|
|
49
|
+
const [selectedStatus, setSelectedStatus] = react.useState("pending");
|
|
50
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs(ui.ProgressAccordion.Item, { value: review.id, className: "px-4", children: [
|
|
51
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
52
|
+
ui.ProgressAccordion.Header,
|
|
53
|
+
{
|
|
54
|
+
status: statusMap[review.status],
|
|
55
|
+
className: "pl-0",
|
|
56
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
57
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color, children: review.status.charAt(0).toUpperCase() + review.status.slice(1) }),
|
|
58
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "w-64 truncate", children: review.title })
|
|
59
|
+
] })
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.ProgressAccordion.Content, { className: "pl-16 pb-4", children: [
|
|
63
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
64
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "font-medium", children: review.rating ? `Rating: ${review.rating}` : null }),
|
|
65
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: "small", className: "text-ui-fg-muted", children: new Date(review.created_at).toLocaleDateString("en-GB") })
|
|
66
|
+
] }),
|
|
67
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
68
|
+
reactRouterDom.Link,
|
|
69
|
+
{
|
|
70
|
+
className: "text-ui-fg-muted hover:text-ui-fg-on-color",
|
|
71
|
+
to: review.customer_id ? `/customers/${review.customer_id}` : "",
|
|
72
|
+
children: [
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: "User:" }),
|
|
74
|
+
" ",
|
|
75
|
+
review.is_admin ? "Admin" : ((_a = review.customer) == null ? void 0 : _a.first_name) ?? review.customer_id
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap my-2", children: review.content }),
|
|
80
|
+
Boolean((_b = review.medias) == null ? void 0 : _b.length) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 my-2", children: (_c = review.medias) == null ? void 0 : _c.map((media) => /* @__PURE__ */ jsxRuntime.jsx(MediaViewer, { media }, media.id)) }),
|
|
81
|
+
review.status === "pending" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 justify-end mt-2", children: [
|
|
82
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
83
|
+
ui.Button,
|
|
84
|
+
{
|
|
85
|
+
variant: "danger",
|
|
86
|
+
disabled: selectedStatus === "rejected",
|
|
87
|
+
onClick: () => {
|
|
88
|
+
setSelectedStatus("rejected");
|
|
89
|
+
onAction == null ? void 0 : onAction(review.id, "reject");
|
|
90
|
+
},
|
|
91
|
+
children: "Reject"
|
|
92
|
+
}
|
|
93
|
+
),
|
|
94
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
95
|
+
ui.Button,
|
|
96
|
+
{
|
|
97
|
+
variant: "primary",
|
|
98
|
+
disabled: selectedStatus === "approved",
|
|
99
|
+
onClick: () => {
|
|
100
|
+
setSelectedStatus("approved");
|
|
101
|
+
onAction == null ? void 0 : onAction(review.id, "approve");
|
|
102
|
+
},
|
|
103
|
+
children: "Approve"
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
] })
|
|
107
|
+
] })
|
|
108
|
+
] }) });
|
|
109
|
+
};
|
|
110
|
+
const columnHelper = ui.createDataTableColumnHelper();
|
|
111
|
+
const reviewColumns = [
|
|
112
|
+
columnHelper.select(),
|
|
113
|
+
columnHelper.accessor("product", {
|
|
114
|
+
header: "Product",
|
|
115
|
+
id: "product",
|
|
116
|
+
cell: ({ row }) => {
|
|
117
|
+
var _a;
|
|
118
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: `/products/${row.original.product_id}`, children: (_a = row.original.product) == null ? void 0 : _a.title });
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
columnHelper.accessor("children", {
|
|
122
|
+
header: "Pending Replies",
|
|
123
|
+
id: "pending-replies",
|
|
124
|
+
cell: ({ row }) => {
|
|
125
|
+
var _a;
|
|
126
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
127
|
+
((_a = row.original.children) == null ? void 0 : _a.filter((r) => r.status === "pending").length) || 0,
|
|
128
|
+
" ",
|
|
129
|
+
"Replies"
|
|
130
|
+
] });
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
columnHelper.accessor("children", {
|
|
134
|
+
header: "Replies",
|
|
135
|
+
id: "replies",
|
|
136
|
+
cell: ({ row }) => {
|
|
137
|
+
var _a;
|
|
138
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
139
|
+
((_a = row.original.children) == null ? void 0 : _a.length) || 0,
|
|
140
|
+
" Replies"
|
|
141
|
+
] });
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
144
|
+
columnHelper.accessor("status", {
|
|
145
|
+
header: "Status",
|
|
146
|
+
id: "status",
|
|
147
|
+
cell: ({ row }) => {
|
|
148
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: statusColor(row.original.status), children: row.original.status.charAt(0).toUpperCase() + row.original.status.slice(1) });
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
columnHelper.accessor("rating", {
|
|
152
|
+
header: "Rating",
|
|
153
|
+
id: "rating"
|
|
154
|
+
}),
|
|
155
|
+
columnHelper.accessor("title", {
|
|
156
|
+
header: "Title",
|
|
157
|
+
id: "title",
|
|
158
|
+
maxSize: 150,
|
|
159
|
+
cell: ({ row }) => {
|
|
160
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate", children: row.original.title });
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
columnHelper.accessor("content", {
|
|
164
|
+
header: "Content",
|
|
165
|
+
id: "content",
|
|
166
|
+
maxSize: 200,
|
|
167
|
+
cell: ({ row }) => {
|
|
168
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate", children: row.original.content });
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
];
|
|
172
|
+
const ReviewTable = ({ title, table }) => {
|
|
173
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Container, { children: [
|
|
174
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.DataTable, { instance: table, children: [
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.Toolbar, { className: "flex flex-col items-start justify-between gap-2 md:flex-row md:items-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { children: title }) }),
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.Table, {}),
|
|
177
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.Pagination, {}),
|
|
178
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DataTable.CommandBar, { selectedLabel: (count) => `${count} selected` })
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Toaster, {})
|
|
181
|
+
] });
|
|
182
|
+
};
|
|
183
|
+
const sdk = new Medusa__default.default({
|
|
184
|
+
baseUrl: "http://localhost:9000",
|
|
185
|
+
debug: process.env.NODE_ENV === "development",
|
|
186
|
+
auth: {
|
|
187
|
+
type: "session"
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
const commandHelper = ui.createDataTableCommandHelper();
|
|
191
|
+
const useCommands = (refetch) => {
|
|
192
|
+
return [
|
|
193
|
+
commandHelper.command({
|
|
194
|
+
label: "Approve",
|
|
195
|
+
shortcut: "A",
|
|
196
|
+
action: async (selection) => {
|
|
197
|
+
const reviewsToApproveIds = Object.keys(selection);
|
|
198
|
+
sdk.client.fetch("/admin/reviews/status", {
|
|
199
|
+
method: "POST",
|
|
200
|
+
body: {
|
|
201
|
+
ids: reviewsToApproveIds,
|
|
202
|
+
status: "approved"
|
|
203
|
+
}
|
|
204
|
+
}).then(() => {
|
|
205
|
+
ui.toast.success("Reviews approved");
|
|
206
|
+
refetch();
|
|
207
|
+
}).catch(() => {
|
|
208
|
+
ui.toast.error("Failed to approve reviews");
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}),
|
|
212
|
+
commandHelper.command({
|
|
213
|
+
label: "Reject",
|
|
214
|
+
shortcut: "R",
|
|
215
|
+
action: async (selection) => {
|
|
216
|
+
const reviewsToRejectIds = Object.keys(selection);
|
|
217
|
+
sdk.client.fetch("/admin/reviews/status", {
|
|
218
|
+
method: "POST",
|
|
219
|
+
body: {
|
|
220
|
+
ids: reviewsToRejectIds,
|
|
221
|
+
status: "rejected"
|
|
222
|
+
}
|
|
223
|
+
}).then(() => {
|
|
224
|
+
ui.toast.success("Reviews rejected");
|
|
225
|
+
refetch();
|
|
226
|
+
}).catch(() => {
|
|
227
|
+
ui.toast.error("Failed to reject reviews");
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
];
|
|
232
|
+
};
|
|
233
|
+
const limit = 20;
|
|
234
|
+
const ReviewsPage = () => {
|
|
235
|
+
var _a, _b;
|
|
236
|
+
const [pagination, setPagination] = react.useState({
|
|
237
|
+
pageSize: limit,
|
|
238
|
+
pageIndex: 0
|
|
239
|
+
});
|
|
240
|
+
const offset = react.useMemo(() => {
|
|
241
|
+
return pagination.pageIndex * limit;
|
|
242
|
+
}, [pagination]);
|
|
243
|
+
const [rowSelection, setRowSelection] = react.useState(
|
|
244
|
+
{}
|
|
245
|
+
);
|
|
246
|
+
const [isDrawerOpen, setIsDrawerOpen] = react.useState(false);
|
|
247
|
+
const [selectedReview, setSelectedReview] = react.useState(null);
|
|
248
|
+
const [replyContent, setReplyContent] = react.useState("");
|
|
249
|
+
const pendingReplies = react.useMemo(() => {
|
|
250
|
+
var _a2;
|
|
251
|
+
const ids = ((_a2 = selectedReview == null ? void 0 : selectedReview.children) == null ? void 0 : _a2.filter((child) => child.status === "pending")) ?? [];
|
|
252
|
+
if ((selectedReview == null ? void 0 : selectedReview.status) === "pending") {
|
|
253
|
+
ids.push(selectedReview);
|
|
254
|
+
}
|
|
255
|
+
return ids;
|
|
256
|
+
}, [selectedReview]);
|
|
257
|
+
const [reviews, setReviews] = react.useState(null);
|
|
258
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
259
|
+
const [error, setError] = react.useState(null);
|
|
260
|
+
const fetchReviews = async () => {
|
|
261
|
+
setIsLoading(true);
|
|
262
|
+
setError(null);
|
|
263
|
+
try {
|
|
264
|
+
const { data } = await axios__default.default.get("/admin/reviews", {
|
|
265
|
+
params: {
|
|
266
|
+
offset: pagination.pageIndex * pagination.pageSize,
|
|
267
|
+
limit: pagination.pageSize,
|
|
268
|
+
order: "-created_at"
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
setReviews(data);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
setError(
|
|
274
|
+
err instanceof Error ? err : new Error("Failed to fetch reviews")
|
|
275
|
+
);
|
|
276
|
+
} finally {
|
|
277
|
+
setIsLoading(false);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
react.useEffect(() => {
|
|
281
|
+
fetchReviews();
|
|
282
|
+
}, [offset, limit, pagination.pageIndex, pagination.pageSize]);
|
|
283
|
+
const refetch = fetchReviews;
|
|
284
|
+
const users = react.useMemo(() => {
|
|
285
|
+
var _a2;
|
|
286
|
+
const _users = reviews == null ? void 0 : reviews.reviews.filter((review) => review.product_id === (selectedReview == null ? void 0 : selectedReview.product_id)).map((review) => {
|
|
287
|
+
var _a3;
|
|
288
|
+
const children = review.children || [];
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
id: review.is_admin ? "Admin" : review.customer_id,
|
|
292
|
+
firstName: review.is_admin ? "Admin" : ((_a3 = review.customer) == null ? void 0 : _a3.first_name) ?? "Customer"
|
|
293
|
+
},
|
|
294
|
+
...children.map((child) => {
|
|
295
|
+
var _a4;
|
|
296
|
+
return {
|
|
297
|
+
id: child.is_admin ? "Admin" : child.customer_id,
|
|
298
|
+
firstName: child.is_admin ? "Admin" : ((_a4 = child.customer) == null ? void 0 : _a4.first_name) ?? "Customer"
|
|
299
|
+
};
|
|
300
|
+
})
|
|
301
|
+
];
|
|
302
|
+
}).flat();
|
|
303
|
+
const userMap = new Map((_a2 = _users == null ? void 0 : _users.map) == null ? void 0 : _a2.call(_users, (u) => [u.id, u]));
|
|
304
|
+
const users2 = Array.from(userMap.values());
|
|
305
|
+
return users2.filter((u) => u.id);
|
|
306
|
+
}, [reviews, selectedReview]);
|
|
307
|
+
const commands = useCommands(refetch);
|
|
308
|
+
const table = ui.useDataTable({
|
|
309
|
+
commands,
|
|
310
|
+
columns: reviewColumns,
|
|
311
|
+
data: (reviews == null ? void 0 : reviews.reviews) || [],
|
|
312
|
+
rowCount: (reviews == null ? void 0 : reviews.count) || 0,
|
|
313
|
+
isLoading,
|
|
314
|
+
pagination: {
|
|
315
|
+
state: pagination,
|
|
316
|
+
onPaginationChange: setPagination
|
|
317
|
+
},
|
|
318
|
+
getRowId: (row) => row.id,
|
|
319
|
+
rowSelection: {
|
|
320
|
+
state: rowSelection,
|
|
321
|
+
onRowSelectionChange: setRowSelection
|
|
322
|
+
},
|
|
323
|
+
onRowClick: (_, row) => {
|
|
324
|
+
setSelectedReview(row.original);
|
|
325
|
+
setIsDrawerOpen(true);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
const [toBeSaved, setToBeSaved] = react.useState(/* @__PURE__ */ new Map());
|
|
329
|
+
function handleAddToBeSaved(id, action) {
|
|
330
|
+
setToBeSaved((prev) => {
|
|
331
|
+
const newSet = new Map(prev);
|
|
332
|
+
newSet.set(id, action);
|
|
333
|
+
return newSet;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async function handleSave() {
|
|
337
|
+
const { approved, rejected } = Array.from(toBeSaved.entries()).reduce(
|
|
338
|
+
(acc, [id, action]) => {
|
|
339
|
+
if (action === "approve") {
|
|
340
|
+
acc.approved.push(id);
|
|
341
|
+
} else if (action === "reject") {
|
|
342
|
+
acc.rejected.push(id);
|
|
343
|
+
}
|
|
344
|
+
return acc;
|
|
345
|
+
},
|
|
346
|
+
{ approved: [], rejected: [] }
|
|
347
|
+
);
|
|
348
|
+
await Promise.all([
|
|
349
|
+
axios__default.default.post("/admin/reviews/status", {
|
|
350
|
+
ids: approved,
|
|
351
|
+
status: "approved"
|
|
352
|
+
}),
|
|
353
|
+
axios__default.default.post("/admin/reviews/status", {
|
|
354
|
+
ids: rejected,
|
|
355
|
+
status: "rejected"
|
|
356
|
+
})
|
|
357
|
+
]);
|
|
358
|
+
ui.toast.success("Reviews saved", {
|
|
359
|
+
description: `${approved.length} reviews approved, ${rejected.length} reviews rejected`
|
|
360
|
+
});
|
|
361
|
+
setIsDrawerOpen(false);
|
|
362
|
+
handleResetSelection();
|
|
363
|
+
refetch();
|
|
364
|
+
}
|
|
365
|
+
async function handleReply() {
|
|
366
|
+
await axios__default.default.post("/admin/reviews", {
|
|
367
|
+
product_id: selectedReview == null ? void 0 : selectedReview.product_id,
|
|
368
|
+
parent_id: selectedReview == null ? void 0 : selectedReview.id,
|
|
369
|
+
title: "Reply from Admin",
|
|
370
|
+
content: replyContent
|
|
371
|
+
});
|
|
372
|
+
ui.toast.success("Reply sent");
|
|
373
|
+
refetch();
|
|
374
|
+
setIsDrawerOpen(false);
|
|
375
|
+
handleResetSelection();
|
|
376
|
+
}
|
|
377
|
+
function handleDrawerChange(open) {
|
|
378
|
+
setIsDrawerOpen(open);
|
|
379
|
+
if (!open) {
|
|
380
|
+
handleResetSelection();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function handleResetSelection() {
|
|
384
|
+
setSelectedReview(null);
|
|
385
|
+
setToBeSaved(/* @__PURE__ */ new Map());
|
|
386
|
+
setReplyContent("");
|
|
387
|
+
}
|
|
388
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
389
|
+
/* @__PURE__ */ jsxRuntime.jsx(ReviewTable, { title: "Reviews", table }),
|
|
390
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer, { open: isDrawerOpen, onOpenChange: handleDrawerChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Content, { children: [
|
|
391
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Header, { children: [
|
|
392
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Title, { children: [
|
|
393
|
+
"Review for ",
|
|
394
|
+
(_a = selectedReview == null ? void 0 : selectedReview.product) == null ? void 0 : _a.title
|
|
395
|
+
] }),
|
|
396
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Description, { className: "pt-2", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { onClick: handleSave, disabled: toBeSaved.size <= 0, children: "Approve and Reject replies" }) })
|
|
397
|
+
] }),
|
|
398
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Drawer.Body, { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
399
|
+
ui.ProgressAccordion,
|
|
400
|
+
{
|
|
401
|
+
type: "multiple",
|
|
402
|
+
defaultValue: (pendingReplies == null ? void 0 : pendingReplies.map((child) => child.id)) || [],
|
|
403
|
+
children: [
|
|
404
|
+
selectedReview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
405
|
+
ReviewChild,
|
|
406
|
+
{
|
|
407
|
+
review: selectedReview,
|
|
408
|
+
onAction: (id, action) => {
|
|
409
|
+
handleAddToBeSaved(id, action);
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
selectedReview == null ? void 0 : selectedReview.id
|
|
413
|
+
),
|
|
414
|
+
(_b = selectedReview == null ? void 0 : selectedReview.children) == null ? void 0 : _b.map((child) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
415
|
+
ReviewChild,
|
|
416
|
+
{
|
|
417
|
+
review: child,
|
|
418
|
+
onAction: (id, action) => {
|
|
419
|
+
handleAddToBeSaved(id, action);
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
child.id
|
|
423
|
+
))
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
) }),
|
|
427
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Drawer.Footer, { className: "flex flex-col gap-2", children: [
|
|
428
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
429
|
+
Mentions__default.default,
|
|
430
|
+
{
|
|
431
|
+
rows: 3,
|
|
432
|
+
options: users.map((user) => ({
|
|
433
|
+
value: user.firstName,
|
|
434
|
+
label: user.firstName
|
|
435
|
+
})),
|
|
436
|
+
value: replyContent,
|
|
437
|
+
onChange: (text) => setReplyContent(text),
|
|
438
|
+
notFoundContent: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-ui-bg-subtle", children: "No users found" }),
|
|
439
|
+
className: "w-[98%] text-ui-on-color bg-ui-bg-subtle text-base",
|
|
440
|
+
placement: "top",
|
|
441
|
+
prefix: "@",
|
|
442
|
+
placeholder: "Type @ to mention a user"
|
|
443
|
+
}
|
|
444
|
+
),
|
|
445
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
446
|
+
ui.Button,
|
|
447
|
+
{
|
|
448
|
+
className: "w-[98%]",
|
|
449
|
+
onClick: handleReply,
|
|
450
|
+
disabled: !replyContent,
|
|
451
|
+
children: "Reply as Admin"
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
] })
|
|
455
|
+
] }) })
|
|
456
|
+
] });
|
|
457
|
+
};
|
|
458
|
+
const config = adminSdk.defineRouteConfig({
|
|
459
|
+
label: "Reviews",
|
|
460
|
+
icon: icons.ChatBubbleLeftRight
|
|
461
|
+
});
|
|
462
|
+
const widgetModule = { widgets: [] };
|
|
463
|
+
const routeModule = {
|
|
464
|
+
routes: [
|
|
465
|
+
{
|
|
466
|
+
Component: ReviewsPage,
|
|
467
|
+
path: "/reviews"
|
|
468
|
+
}
|
|
469
|
+
]
|
|
470
|
+
};
|
|
471
|
+
const menuItemModule = {
|
|
472
|
+
menuItems: [
|
|
473
|
+
{
|
|
474
|
+
label: config.label,
|
|
475
|
+
icon: config.icon,
|
|
476
|
+
path: "/reviews",
|
|
477
|
+
nested: void 0
|
|
478
|
+
}
|
|
479
|
+
]
|
|
480
|
+
};
|
|
481
|
+
const formModule = { customFields: {} };
|
|
482
|
+
const displayModule = {
|
|
483
|
+
displays: {}
|
|
484
|
+
};
|
|
485
|
+
const i18nModule = { resources: {} };
|
|
486
|
+
const plugin = {
|
|
487
|
+
widgetModule,
|
|
488
|
+
routeModule,
|
|
489
|
+
menuItemModule,
|
|
490
|
+
formModule,
|
|
491
|
+
displayModule,
|
|
492
|
+
i18nModule
|
|
493
|
+
};
|
|
494
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { jsx, Fragment, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineRouteConfig } from "@medusajs/admin-sdk";
|
|
3
|
+
import { ChatBubbleLeftRight } from "@medusajs/icons";
|
|
4
|
+
import { FocusModal, clx, ProgressAccordion, StatusBadge, Text, Button, createDataTableColumnHelper, Container, DataTable, Heading, Toaster, createDataTableCommandHelper, toast, useDataTable, Drawer } from "@medusajs/ui";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import Mentions from "rc-mentions";
|
|
7
|
+
import { useState, useMemo, useEffect } from "react";
|
|
8
|
+
import { Link } from "react-router-dom";
|
|
9
|
+
import Medusa from "@medusajs/js-sdk";
|
|
10
|
+
import "@medusajs/admin-shared";
|
|
11
|
+
const statusColor = (status) => status === "approved" ? "green" : status === "rejected" ? "red" : "grey";
|
|
12
|
+
const mimeTypes = ["image", "video"];
|
|
13
|
+
const MediaViewer = ({ media, className }) => {
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const mimeType = media.mimeType.split("/")[0];
|
|
16
|
+
if (!mimeTypes.includes(mimeType)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(FocusModal, { open, onOpenChange: setOpen, children: [
|
|
20
|
+
/* @__PURE__ */ jsx(FocusModal.Trigger, { className: "bg-ui-bg-overlay ", children: /* @__PURE__ */ jsxs("div", { className: clx("w-24 h-24", className), children: [
|
|
21
|
+
mimeType === "image" && /* @__PURE__ */ jsx("div", { className: "p-0 w-full h-full object-contain flex items-center justify-center", children: /* @__PURE__ */ jsx("img", { src: media.fileUrl, alt: "an image" }) }),
|
|
22
|
+
mimeType === "video" && /* @__PURE__ */ jsx("div", { className: "p-2 w-full h-full flex items-center justify-center", children: /* @__PURE__ */ jsx(Fragment, { children: "Watch Video" }) })
|
|
23
|
+
] }) }),
|
|
24
|
+
/* @__PURE__ */ jsxs(FocusModal.Content, { className: "w-[80vw] h-[80vh] mx-auto my-auto", children: [
|
|
25
|
+
/* @__PURE__ */ jsxs(FocusModal.Header, { children: [
|
|
26
|
+
/* @__PURE__ */ jsx(FocusModal.Title, { children: media.fileId }),
|
|
27
|
+
/* @__PURE__ */ jsx(FocusModal.Close, {})
|
|
28
|
+
] }),
|
|
29
|
+
/* @__PURE__ */ jsxs(FocusModal.Body, { className: "flex items-center justify-center", children: [
|
|
30
|
+
mimeType === "image" && /* @__PURE__ */ jsx("img", { src: media.fileUrl, alt: "an image" }),
|
|
31
|
+
mimeType === "video" && /* @__PURE__ */ jsx("video", { src: media.fileUrl, controls: true })
|
|
32
|
+
] })
|
|
33
|
+
] })
|
|
34
|
+
] }) });
|
|
35
|
+
};
|
|
36
|
+
const statusMap = {
|
|
37
|
+
pending: "in-progress",
|
|
38
|
+
approved: "completed",
|
|
39
|
+
rejected: "completed"
|
|
40
|
+
};
|
|
41
|
+
const ReviewChild = ({ review, onAction }) => {
|
|
42
|
+
var _a, _b, _c;
|
|
43
|
+
const color = statusColor(review.status);
|
|
44
|
+
const [selectedStatus, setSelectedStatus] = useState("pending");
|
|
45
|
+
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(ProgressAccordion.Item, { value: review.id, className: "px-4", children: [
|
|
46
|
+
/* @__PURE__ */ jsx(
|
|
47
|
+
ProgressAccordion.Header,
|
|
48
|
+
{
|
|
49
|
+
status: statusMap[review.status],
|
|
50
|
+
className: "pl-0",
|
|
51
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
52
|
+
/* @__PURE__ */ jsx(StatusBadge, { color, children: review.status.charAt(0).toUpperCase() + review.status.slice(1) }),
|
|
53
|
+
/* @__PURE__ */ jsx("p", { className: "w-64 truncate", children: review.title })
|
|
54
|
+
] })
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsxs(ProgressAccordion.Content, { className: "pl-16 pb-4", children: [
|
|
58
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "font-medium", children: review.rating ? `Rating: ${review.rating}` : null }),
|
|
60
|
+
/* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: new Date(review.created_at).toLocaleDateString("en-GB") })
|
|
61
|
+
] }),
|
|
62
|
+
/* @__PURE__ */ jsxs(
|
|
63
|
+
Link,
|
|
64
|
+
{
|
|
65
|
+
className: "text-ui-fg-muted hover:text-ui-fg-on-color",
|
|
66
|
+
to: review.customer_id ? `/customers/${review.customer_id}` : "",
|
|
67
|
+
children: [
|
|
68
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "User:" }),
|
|
69
|
+
" ",
|
|
70
|
+
review.is_admin ? "Admin" : ((_a = review.customer) == null ? void 0 : _a.first_name) ?? review.customer_id
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
/* @__PURE__ */ jsx("div", { className: "whitespace-pre-wrap my-2", children: review.content }),
|
|
75
|
+
Boolean((_b = review.medias) == null ? void 0 : _b.length) && /* @__PURE__ */ jsx("div", { className: "flex gap-2 my-2", children: (_c = review.medias) == null ? void 0 : _c.map((media) => /* @__PURE__ */ jsx(MediaViewer, { media }, media.id)) }),
|
|
76
|
+
review.status === "pending" && /* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end mt-2", children: [
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
Button,
|
|
79
|
+
{
|
|
80
|
+
variant: "danger",
|
|
81
|
+
disabled: selectedStatus === "rejected",
|
|
82
|
+
onClick: () => {
|
|
83
|
+
setSelectedStatus("rejected");
|
|
84
|
+
onAction == null ? void 0 : onAction(review.id, "reject");
|
|
85
|
+
},
|
|
86
|
+
children: "Reject"
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
Button,
|
|
91
|
+
{
|
|
92
|
+
variant: "primary",
|
|
93
|
+
disabled: selectedStatus === "approved",
|
|
94
|
+
onClick: () => {
|
|
95
|
+
setSelectedStatus("approved");
|
|
96
|
+
onAction == null ? void 0 : onAction(review.id, "approve");
|
|
97
|
+
},
|
|
98
|
+
children: "Approve"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] })
|
|
102
|
+
] })
|
|
103
|
+
] }) });
|
|
104
|
+
};
|
|
105
|
+
const columnHelper = createDataTableColumnHelper();
|
|
106
|
+
const reviewColumns = [
|
|
107
|
+
columnHelper.select(),
|
|
108
|
+
columnHelper.accessor("product", {
|
|
109
|
+
header: "Product",
|
|
110
|
+
id: "product",
|
|
111
|
+
cell: ({ row }) => {
|
|
112
|
+
var _a;
|
|
113
|
+
return /* @__PURE__ */ jsx(Link, { to: `/products/${row.original.product_id}`, children: (_a = row.original.product) == null ? void 0 : _a.title });
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
columnHelper.accessor("children", {
|
|
117
|
+
header: "Pending Replies",
|
|
118
|
+
id: "pending-replies",
|
|
119
|
+
cell: ({ row }) => {
|
|
120
|
+
var _a;
|
|
121
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
122
|
+
((_a = row.original.children) == null ? void 0 : _a.filter((r) => r.status === "pending").length) || 0,
|
|
123
|
+
" ",
|
|
124
|
+
"Replies"
|
|
125
|
+
] });
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
columnHelper.accessor("children", {
|
|
129
|
+
header: "Replies",
|
|
130
|
+
id: "replies",
|
|
131
|
+
cell: ({ row }) => {
|
|
132
|
+
var _a;
|
|
133
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
134
|
+
((_a = row.original.children) == null ? void 0 : _a.length) || 0,
|
|
135
|
+
" Replies"
|
|
136
|
+
] });
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
columnHelper.accessor("status", {
|
|
140
|
+
header: "Status",
|
|
141
|
+
id: "status",
|
|
142
|
+
cell: ({ row }) => {
|
|
143
|
+
return /* @__PURE__ */ jsx(StatusBadge, { color: statusColor(row.original.status), children: row.original.status.charAt(0).toUpperCase() + row.original.status.slice(1) });
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
columnHelper.accessor("rating", {
|
|
147
|
+
header: "Rating",
|
|
148
|
+
id: "rating"
|
|
149
|
+
}),
|
|
150
|
+
columnHelper.accessor("title", {
|
|
151
|
+
header: "Title",
|
|
152
|
+
id: "title",
|
|
153
|
+
maxSize: 150,
|
|
154
|
+
cell: ({ row }) => {
|
|
155
|
+
return /* @__PURE__ */ jsx("div", { className: "truncate", children: row.original.title });
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
columnHelper.accessor("content", {
|
|
159
|
+
header: "Content",
|
|
160
|
+
id: "content",
|
|
161
|
+
maxSize: 200,
|
|
162
|
+
cell: ({ row }) => {
|
|
163
|
+
return /* @__PURE__ */ jsx("div", { className: "truncate", children: row.original.content });
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
];
|
|
167
|
+
const ReviewTable = ({ title, table }) => {
|
|
168
|
+
return /* @__PURE__ */ jsxs(Container, { children: [
|
|
169
|
+
/* @__PURE__ */ jsxs(DataTable, { instance: table, children: [
|
|
170
|
+
/* @__PURE__ */ jsx(DataTable.Toolbar, { className: "flex flex-col items-start justify-between gap-2 md:flex-row md:items-center", children: /* @__PURE__ */ jsx(Heading, { children: title }) }),
|
|
171
|
+
/* @__PURE__ */ jsx(DataTable.Table, {}),
|
|
172
|
+
/* @__PURE__ */ jsx(DataTable.Pagination, {}),
|
|
173
|
+
/* @__PURE__ */ jsx(DataTable.CommandBar, { selectedLabel: (count) => `${count} selected` })
|
|
174
|
+
] }),
|
|
175
|
+
/* @__PURE__ */ jsx(Toaster, {})
|
|
176
|
+
] });
|
|
177
|
+
};
|
|
178
|
+
const sdk = new Medusa({
|
|
179
|
+
baseUrl: "http://localhost:9000",
|
|
180
|
+
debug: process.env.NODE_ENV === "development",
|
|
181
|
+
auth: {
|
|
182
|
+
type: "session"
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
const commandHelper = createDataTableCommandHelper();
|
|
186
|
+
const useCommands = (refetch) => {
|
|
187
|
+
return [
|
|
188
|
+
commandHelper.command({
|
|
189
|
+
label: "Approve",
|
|
190
|
+
shortcut: "A",
|
|
191
|
+
action: async (selection) => {
|
|
192
|
+
const reviewsToApproveIds = Object.keys(selection);
|
|
193
|
+
sdk.client.fetch("/admin/reviews/status", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: {
|
|
196
|
+
ids: reviewsToApproveIds,
|
|
197
|
+
status: "approved"
|
|
198
|
+
}
|
|
199
|
+
}).then(() => {
|
|
200
|
+
toast.success("Reviews approved");
|
|
201
|
+
refetch();
|
|
202
|
+
}).catch(() => {
|
|
203
|
+
toast.error("Failed to approve reviews");
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}),
|
|
207
|
+
commandHelper.command({
|
|
208
|
+
label: "Reject",
|
|
209
|
+
shortcut: "R",
|
|
210
|
+
action: async (selection) => {
|
|
211
|
+
const reviewsToRejectIds = Object.keys(selection);
|
|
212
|
+
sdk.client.fetch("/admin/reviews/status", {
|
|
213
|
+
method: "POST",
|
|
214
|
+
body: {
|
|
215
|
+
ids: reviewsToRejectIds,
|
|
216
|
+
status: "rejected"
|
|
217
|
+
}
|
|
218
|
+
}).then(() => {
|
|
219
|
+
toast.success("Reviews rejected");
|
|
220
|
+
refetch();
|
|
221
|
+
}).catch(() => {
|
|
222
|
+
toast.error("Failed to reject reviews");
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
];
|
|
227
|
+
};
|
|
228
|
+
const limit = 20;
|
|
229
|
+
const ReviewsPage = () => {
|
|
230
|
+
var _a, _b;
|
|
231
|
+
const [pagination, setPagination] = useState({
|
|
232
|
+
pageSize: limit,
|
|
233
|
+
pageIndex: 0
|
|
234
|
+
});
|
|
235
|
+
const offset = useMemo(() => {
|
|
236
|
+
return pagination.pageIndex * limit;
|
|
237
|
+
}, [pagination]);
|
|
238
|
+
const [rowSelection, setRowSelection] = useState(
|
|
239
|
+
{}
|
|
240
|
+
);
|
|
241
|
+
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
242
|
+
const [selectedReview, setSelectedReview] = useState(null);
|
|
243
|
+
const [replyContent, setReplyContent] = useState("");
|
|
244
|
+
const pendingReplies = useMemo(() => {
|
|
245
|
+
var _a2;
|
|
246
|
+
const ids = ((_a2 = selectedReview == null ? void 0 : selectedReview.children) == null ? void 0 : _a2.filter((child) => child.status === "pending")) ?? [];
|
|
247
|
+
if ((selectedReview == null ? void 0 : selectedReview.status) === "pending") {
|
|
248
|
+
ids.push(selectedReview);
|
|
249
|
+
}
|
|
250
|
+
return ids;
|
|
251
|
+
}, [selectedReview]);
|
|
252
|
+
const [reviews, setReviews] = useState(null);
|
|
253
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
254
|
+
const [error, setError] = useState(null);
|
|
255
|
+
const fetchReviews = async () => {
|
|
256
|
+
setIsLoading(true);
|
|
257
|
+
setError(null);
|
|
258
|
+
try {
|
|
259
|
+
const { data } = await axios.get("/admin/reviews", {
|
|
260
|
+
params: {
|
|
261
|
+
offset: pagination.pageIndex * pagination.pageSize,
|
|
262
|
+
limit: pagination.pageSize,
|
|
263
|
+
order: "-created_at"
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
setReviews(data);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
setError(
|
|
269
|
+
err instanceof Error ? err : new Error("Failed to fetch reviews")
|
|
270
|
+
);
|
|
271
|
+
} finally {
|
|
272
|
+
setIsLoading(false);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
fetchReviews();
|
|
277
|
+
}, [offset, limit, pagination.pageIndex, pagination.pageSize]);
|
|
278
|
+
const refetch = fetchReviews;
|
|
279
|
+
const users = useMemo(() => {
|
|
280
|
+
var _a2;
|
|
281
|
+
const _users = reviews == null ? void 0 : reviews.reviews.filter((review) => review.product_id === (selectedReview == null ? void 0 : selectedReview.product_id)).map((review) => {
|
|
282
|
+
var _a3;
|
|
283
|
+
const children = review.children || [];
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
id: review.is_admin ? "Admin" : review.customer_id,
|
|
287
|
+
firstName: review.is_admin ? "Admin" : ((_a3 = review.customer) == null ? void 0 : _a3.first_name) ?? "Customer"
|
|
288
|
+
},
|
|
289
|
+
...children.map((child) => {
|
|
290
|
+
var _a4;
|
|
291
|
+
return {
|
|
292
|
+
id: child.is_admin ? "Admin" : child.customer_id,
|
|
293
|
+
firstName: child.is_admin ? "Admin" : ((_a4 = child.customer) == null ? void 0 : _a4.first_name) ?? "Customer"
|
|
294
|
+
};
|
|
295
|
+
})
|
|
296
|
+
];
|
|
297
|
+
}).flat();
|
|
298
|
+
const userMap = new Map((_a2 = _users == null ? void 0 : _users.map) == null ? void 0 : _a2.call(_users, (u) => [u.id, u]));
|
|
299
|
+
const users2 = Array.from(userMap.values());
|
|
300
|
+
return users2.filter((u) => u.id);
|
|
301
|
+
}, [reviews, selectedReview]);
|
|
302
|
+
const commands = useCommands(refetch);
|
|
303
|
+
const table = useDataTable({
|
|
304
|
+
commands,
|
|
305
|
+
columns: reviewColumns,
|
|
306
|
+
data: (reviews == null ? void 0 : reviews.reviews) || [],
|
|
307
|
+
rowCount: (reviews == null ? void 0 : reviews.count) || 0,
|
|
308
|
+
isLoading,
|
|
309
|
+
pagination: {
|
|
310
|
+
state: pagination,
|
|
311
|
+
onPaginationChange: setPagination
|
|
312
|
+
},
|
|
313
|
+
getRowId: (row) => row.id,
|
|
314
|
+
rowSelection: {
|
|
315
|
+
state: rowSelection,
|
|
316
|
+
onRowSelectionChange: setRowSelection
|
|
317
|
+
},
|
|
318
|
+
onRowClick: (_, row) => {
|
|
319
|
+
setSelectedReview(row.original);
|
|
320
|
+
setIsDrawerOpen(true);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
const [toBeSaved, setToBeSaved] = useState(/* @__PURE__ */ new Map());
|
|
324
|
+
function handleAddToBeSaved(id, action) {
|
|
325
|
+
setToBeSaved((prev) => {
|
|
326
|
+
const newSet = new Map(prev);
|
|
327
|
+
newSet.set(id, action);
|
|
328
|
+
return newSet;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
async function handleSave() {
|
|
332
|
+
const { approved, rejected } = Array.from(toBeSaved.entries()).reduce(
|
|
333
|
+
(acc, [id, action]) => {
|
|
334
|
+
if (action === "approve") {
|
|
335
|
+
acc.approved.push(id);
|
|
336
|
+
} else if (action === "reject") {
|
|
337
|
+
acc.rejected.push(id);
|
|
338
|
+
}
|
|
339
|
+
return acc;
|
|
340
|
+
},
|
|
341
|
+
{ approved: [], rejected: [] }
|
|
342
|
+
);
|
|
343
|
+
await Promise.all([
|
|
344
|
+
axios.post("/admin/reviews/status", {
|
|
345
|
+
ids: approved,
|
|
346
|
+
status: "approved"
|
|
347
|
+
}),
|
|
348
|
+
axios.post("/admin/reviews/status", {
|
|
349
|
+
ids: rejected,
|
|
350
|
+
status: "rejected"
|
|
351
|
+
})
|
|
352
|
+
]);
|
|
353
|
+
toast.success("Reviews saved", {
|
|
354
|
+
description: `${approved.length} reviews approved, ${rejected.length} reviews rejected`
|
|
355
|
+
});
|
|
356
|
+
setIsDrawerOpen(false);
|
|
357
|
+
handleResetSelection();
|
|
358
|
+
refetch();
|
|
359
|
+
}
|
|
360
|
+
async function handleReply() {
|
|
361
|
+
await axios.post("/admin/reviews", {
|
|
362
|
+
product_id: selectedReview == null ? void 0 : selectedReview.product_id,
|
|
363
|
+
parent_id: selectedReview == null ? void 0 : selectedReview.id,
|
|
364
|
+
title: "Reply from Admin",
|
|
365
|
+
content: replyContent
|
|
366
|
+
});
|
|
367
|
+
toast.success("Reply sent");
|
|
368
|
+
refetch();
|
|
369
|
+
setIsDrawerOpen(false);
|
|
370
|
+
handleResetSelection();
|
|
371
|
+
}
|
|
372
|
+
function handleDrawerChange(open) {
|
|
373
|
+
setIsDrawerOpen(open);
|
|
374
|
+
if (!open) {
|
|
375
|
+
handleResetSelection();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function handleResetSelection() {
|
|
379
|
+
setSelectedReview(null);
|
|
380
|
+
setToBeSaved(/* @__PURE__ */ new Map());
|
|
381
|
+
setReplyContent("");
|
|
382
|
+
}
|
|
383
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
384
|
+
/* @__PURE__ */ jsx(ReviewTable, { title: "Reviews", table }),
|
|
385
|
+
/* @__PURE__ */ jsx(Drawer, { open: isDrawerOpen, onOpenChange: handleDrawerChange, children: /* @__PURE__ */ jsxs(Drawer.Content, { children: [
|
|
386
|
+
/* @__PURE__ */ jsxs(Drawer.Header, { children: [
|
|
387
|
+
/* @__PURE__ */ jsxs(Drawer.Title, { children: [
|
|
388
|
+
"Review for ",
|
|
389
|
+
(_a = selectedReview == null ? void 0 : selectedReview.product) == null ? void 0 : _a.title
|
|
390
|
+
] }),
|
|
391
|
+
/* @__PURE__ */ jsx(Drawer.Description, { className: "pt-2", children: /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: toBeSaved.size <= 0, children: "Approve and Reject replies" }) })
|
|
392
|
+
] }),
|
|
393
|
+
/* @__PURE__ */ jsx(Drawer.Body, { className: "overflow-auto", children: /* @__PURE__ */ jsxs(
|
|
394
|
+
ProgressAccordion,
|
|
395
|
+
{
|
|
396
|
+
type: "multiple",
|
|
397
|
+
defaultValue: (pendingReplies == null ? void 0 : pendingReplies.map((child) => child.id)) || [],
|
|
398
|
+
children: [
|
|
399
|
+
selectedReview && /* @__PURE__ */ jsx(
|
|
400
|
+
ReviewChild,
|
|
401
|
+
{
|
|
402
|
+
review: selectedReview,
|
|
403
|
+
onAction: (id, action) => {
|
|
404
|
+
handleAddToBeSaved(id, action);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
selectedReview == null ? void 0 : selectedReview.id
|
|
408
|
+
),
|
|
409
|
+
(_b = selectedReview == null ? void 0 : selectedReview.children) == null ? void 0 : _b.map((child) => /* @__PURE__ */ jsx(
|
|
410
|
+
ReviewChild,
|
|
411
|
+
{
|
|
412
|
+
review: child,
|
|
413
|
+
onAction: (id, action) => {
|
|
414
|
+
handleAddToBeSaved(id, action);
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
child.id
|
|
418
|
+
))
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
) }),
|
|
422
|
+
/* @__PURE__ */ jsxs(Drawer.Footer, { className: "flex flex-col gap-2", children: [
|
|
423
|
+
/* @__PURE__ */ jsx(
|
|
424
|
+
Mentions,
|
|
425
|
+
{
|
|
426
|
+
rows: 3,
|
|
427
|
+
options: users.map((user) => ({
|
|
428
|
+
value: user.firstName,
|
|
429
|
+
label: user.firstName
|
|
430
|
+
})),
|
|
431
|
+
value: replyContent,
|
|
432
|
+
onChange: (text) => setReplyContent(text),
|
|
433
|
+
notFoundContent: /* @__PURE__ */ jsx("div", { className: "text-ui-bg-subtle", children: "No users found" }),
|
|
434
|
+
className: "w-[98%] text-ui-on-color bg-ui-bg-subtle text-base",
|
|
435
|
+
placement: "top",
|
|
436
|
+
prefix: "@",
|
|
437
|
+
placeholder: "Type @ to mention a user"
|
|
438
|
+
}
|
|
439
|
+
),
|
|
440
|
+
/* @__PURE__ */ jsx(
|
|
441
|
+
Button,
|
|
442
|
+
{
|
|
443
|
+
className: "w-[98%]",
|
|
444
|
+
onClick: handleReply,
|
|
445
|
+
disabled: !replyContent,
|
|
446
|
+
children: "Reply as Admin"
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] })
|
|
450
|
+
] }) })
|
|
451
|
+
] });
|
|
452
|
+
};
|
|
453
|
+
const config = defineRouteConfig({
|
|
454
|
+
label: "Reviews",
|
|
455
|
+
icon: ChatBubbleLeftRight
|
|
456
|
+
});
|
|
457
|
+
const widgetModule = { widgets: [] };
|
|
458
|
+
const routeModule = {
|
|
459
|
+
routes: [
|
|
460
|
+
{
|
|
461
|
+
Component: ReviewsPage,
|
|
462
|
+
path: "/reviews"
|
|
463
|
+
}
|
|
464
|
+
]
|
|
465
|
+
};
|
|
466
|
+
const menuItemModule = {
|
|
467
|
+
menuItems: [
|
|
468
|
+
{
|
|
469
|
+
label: config.label,
|
|
470
|
+
icon: config.icon,
|
|
471
|
+
path: "/reviews",
|
|
472
|
+
nested: void 0
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
};
|
|
476
|
+
const formModule = { customFields: {} };
|
|
477
|
+
const displayModule = {
|
|
478
|
+
displays: {}
|
|
479
|
+
};
|
|
480
|
+
const i18nModule = { resources: {} };
|
|
481
|
+
const plugin = {
|
|
482
|
+
widgetModule,
|
|
483
|
+
routeModule,
|
|
484
|
+
menuItemModule,
|
|
485
|
+
formModule,
|
|
486
|
+
displayModule,
|
|
487
|
+
i18nModule
|
|
488
|
+
};
|
|
489
|
+
export {
|
|
490
|
+
plugin as default
|
|
491
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
.rc-mentions {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
position: relative;
|
|
4
|
+
white-space: pre-wrap;
|
|
5
|
+
}
|
|
6
|
+
.rc-mentions > textarea,
|
|
7
|
+
.rc-mentions-measure {
|
|
8
|
+
font-size: inherit;
|
|
9
|
+
font-size-adjust: inherit;
|
|
10
|
+
font-style: inherit;
|
|
11
|
+
font-variant: inherit;
|
|
12
|
+
font-stretch: inherit;
|
|
13
|
+
font-weight: inherit;
|
|
14
|
+
font-family: inherit;
|
|
15
|
+
padding: 0;
|
|
16
|
+
margin: 0;
|
|
17
|
+
line-height: inherit;
|
|
18
|
+
vertical-align: top;
|
|
19
|
+
overflow: inherit;
|
|
20
|
+
word-break: inherit;
|
|
21
|
+
white-space: inherit;
|
|
22
|
+
word-wrap: break-word;
|
|
23
|
+
overflow-x: initial;
|
|
24
|
+
overflow-y: auto;
|
|
25
|
+
text-align: inherit;
|
|
26
|
+
letter-spacing: inherit;
|
|
27
|
+
tab-size: inherit;
|
|
28
|
+
direction: inherit;
|
|
29
|
+
}
|
|
30
|
+
.rc-mentions > textarea {
|
|
31
|
+
border: none;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
.rc-mentions-measure {
|
|
35
|
+
position: absolute;
|
|
36
|
+
left: 0;
|
|
37
|
+
right: 0;
|
|
38
|
+
top: 0;
|
|
39
|
+
bottom: 0;
|
|
40
|
+
pointer-events: none;
|
|
41
|
+
color: transparent;
|
|
42
|
+
z-index: -1;
|
|
43
|
+
}
|
|
44
|
+
.rc-mentions-dropdown {
|
|
45
|
+
position: absolute;
|
|
46
|
+
color: #000;
|
|
47
|
+
}
|
|
48
|
+
.rc-mentions-dropdown-menu {
|
|
49
|
+
list-style: none;
|
|
50
|
+
margin: 0;
|
|
51
|
+
padding: 0;
|
|
52
|
+
}
|
|
53
|
+
.rc-mentions-dropdown-menu-item {
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
.rc-mentions {
|
|
57
|
+
font-size: 20px;
|
|
58
|
+
border: 1px solid #999;
|
|
59
|
+
border-radius: 3px;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
}
|
|
62
|
+
.rc-mentions-dropdown {
|
|
63
|
+
border: 1px solid #999;
|
|
64
|
+
border-radius: 3px;
|
|
65
|
+
background: #fff;
|
|
66
|
+
}
|
|
67
|
+
.rc-mentions-dropdown-menu-item {
|
|
68
|
+
padding: 4px 8px;
|
|
69
|
+
}
|
|
70
|
+
.rc-mentions-dropdown-menu-item-active {
|
|
71
|
+
background: #e6f7ff;
|
|
72
|
+
}
|
|
73
|
+
.rc-mentions-dropdown-menu-item-disabled {
|
|
74
|
+
opacity: 0.5;
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lodashventure/medusa-review",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.15",
|
|
4
4
|
"description": "A starter for Medusa plugins.",
|
|
5
5
|
"author": "Medusa (https://medusajs.com)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "medusa plugin:build",
|
|
28
|
-
"dev": "medusa plugin:develop"
|
|
28
|
+
"dev": "medusa plugin:develop",
|
|
29
|
+
"prepublishOnly": "medusa plugin:build"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@medusajs/admin-sdk": "2.11.2",
|
|
@@ -64,7 +65,8 @@
|
|
|
64
65
|
"node": ">=20"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
68
|
+
"axios": "^1.7.9",
|
|
67
69
|
"multer": "^1.4.5-lts.2",
|
|
68
70
|
"rc-mentions": "^2.20.0"
|
|
69
71
|
}
|
|
70
|
-
}
|
|
72
|
+
}
|