@webx-ui/module-reviews 0.1.2 → 0.1.3
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/dist/index.js +87 -87
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { reorderItems as ge, useAdmin as Z, useTranslate as oe, useErrorText as se,
|
|
2
|
-
import { defineComponent as le, ref as W, computed as y, onMounted as
|
|
3
|
-
import { onBeforeRouteUpdate as
|
|
4
|
-
import { useLocales as
|
|
1
|
+
import { reorderItems as ge, useAdmin as Z, useTranslate as oe, useErrorText as se, useBodyKeys as fe, WxScreenHead as me, WxScreen as be, WxSaveState as xe, useItemOrder as _e, WxListScreen as ke, WxRowMenu as Se, categoryRoutes as We } from "@webx-ui/module-admin";
|
|
2
|
+
import { defineComponent as le, ref as W, computed as y, onMounted as Te, onBeforeUnmount as ne, openBlock as c, createElementBlock as $, createBlock as x, unref as t, Fragment as Q, createVNode as f, createSlots as Re, withCtx as p, createTextVNode as M, toDisplayString as S, createCommentVNode as N, watch as te, createElementVNode as D, normalizeClass as G, resolveDynamicComponent as Ne, renderList as qe } from "vue";
|
|
3
|
+
import { onBeforeRouteUpdate as $e, onBeforeRouteLeave as Ce, useRoute as Be, useRouter as Ve } from "vue-router";
|
|
4
|
+
import { useLocales as Le, localizedValue as Pe, WxSkeleton as ve, WxBadge as F, WxActionBar as Ae, WxButton as ze, toast as _, confirm as X, WxCard as Ie, WxListDetail as Me, WxEmpty as ae, WxInput as Ue, WxIcon as re, WxSelect as De, WxSortableList as Oe, WxAvatar as je, WxTooltip as Ee, WxAlert as Je } from "@webx-ui/core";
|
|
5
5
|
const Y = "reviews";
|
|
6
|
-
function
|
|
6
|
+
function ue(n) {
|
|
7
7
|
const v = `${n.apiPath}/${Y}`, s = (a) => a.data;
|
|
8
8
|
return {
|
|
9
9
|
list: (a = {}) => {
|
|
10
|
-
const
|
|
11
|
-
return a.search &&
|
|
10
|
+
const u = new URLSearchParams();
|
|
11
|
+
return a.search && u.set("search", a.search), a.category != null && u.set("category", String(a.category)), a.trashed && u.set("trashed", "1"), n.http.get(u.size > 0 ? `${v}?${u}` : v);
|
|
12
12
|
},
|
|
13
13
|
get: (a) => n.http.get(`${v}/${a}`).then(s),
|
|
14
14
|
create: (a) => n.http.post(v, { values: a }).then(s),
|
|
15
|
-
save: (a,
|
|
15
|
+
save: (a, u) => n.http.put(`${v}/${a}`, { values: u }).then(s),
|
|
16
16
|
remove: (a) => n.http.delete(`${v}/${a}`).then(() => {
|
|
17
17
|
}),
|
|
18
18
|
// The resource of the list line, and not the form's pair: a restored review stays closed.
|
|
19
19
|
restore: (a) => n.http.post(`${v}/${a}/restore`, {}).then(s),
|
|
20
|
-
reorder: (a,
|
|
20
|
+
reorder: (a, u = null) => ge(n, Y, a, u)
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
const
|
|
23
|
+
const Ke = {
|
|
24
24
|
module: {
|
|
25
25
|
group: "Reviews",
|
|
26
26
|
reviews: "Reviews",
|
|
@@ -101,10 +101,10 @@ const Je = {
|
|
|
101
101
|
saved: "The category is saved."
|
|
102
102
|
}
|
|
103
103
|
}, ie = /* @__PURE__ */ new WeakSet();
|
|
104
|
-
function
|
|
104
|
+
function ce() {
|
|
105
105
|
try {
|
|
106
106
|
const { i18n: n } = Z();
|
|
107
|
-
ie.has(n) || (n.defaults("webx-reviews",
|
|
107
|
+
ie.has(n) || (n.defaults("webx-reviews", Ke), ie.add(n));
|
|
108
108
|
} catch {
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -118,11 +118,11 @@ const Fe = /* @__PURE__ */ le({
|
|
|
118
118
|
},
|
|
119
119
|
emits: ["back", "saved", "created", "removed"],
|
|
120
120
|
setup(n, { emit: v }) {
|
|
121
|
-
const s = n, a = v,
|
|
122
|
-
|
|
123
|
-
const l = oe("webx-reviews"), V = se(), k = W(null), g = W({}), C = W(""), q = W(s.id !== null), d = W(!1), T = W({}), b = y(() =>
|
|
121
|
+
const s = n, a = v, u = Z(), i = ue(u), B = Le();
|
|
122
|
+
ce();
|
|
123
|
+
const l = oe("webx-reviews"), V = se(), k = W(null), g = W({}), C = W(""), q = W(s.id !== null), d = W(!1), T = W({}), b = y(() => u.can("reviews.manage")), L = y(() => JSON.stringify(g.value)), m = y(() => C.value !== "" && L.value !== C.value), O = y(() => d.value ? "saving" : m.value || s.id === null ? "unsaved" : "saved"), H = y(() => {
|
|
124
124
|
const o = g.value.name, h = s.id === null ? l("review.new") : k.value?.name ?? "";
|
|
125
|
-
return
|
|
125
|
+
return Pe(o, B.active.value, h) || l("review.untitled");
|
|
126
126
|
});
|
|
127
127
|
function P(o) {
|
|
128
128
|
k.value = o.review, g.value = o.values, C.value = JSON.stringify(o.values);
|
|
@@ -181,10 +181,10 @@ const Fe = /* @__PURE__ */ le({
|
|
|
181
181
|
function A(o) {
|
|
182
182
|
m.value && o.preventDefault();
|
|
183
183
|
}
|
|
184
|
-
function
|
|
184
|
+
function J(o) {
|
|
185
185
|
(o.ctrlKey || o.metaKey) && o.key.toLowerCase() === "s" && (o.preventDefault(), (m.value || s.id === null) && E());
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
fe(J), Te(() => {
|
|
188
188
|
j(), window.addEventListener("beforeunload", A);
|
|
189
189
|
}), ne(() => {
|
|
190
190
|
window.removeEventListener("beforeunload", A);
|
|
@@ -198,10 +198,10 @@ const Fe = /* @__PURE__ */ le({
|
|
|
198
198
|
tone: "danger"
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
|
-
|
|
201
|
+
$e(
|
|
202
202
|
async (o, h) => o.query.review === h.query.review ? !0 : await z()
|
|
203
|
-
),
|
|
204
|
-
const
|
|
203
|
+
), Ce(z);
|
|
204
|
+
const K = y(
|
|
205
205
|
() => b.value && k.value !== null ? [
|
|
206
206
|
{
|
|
207
207
|
key: "delete",
|
|
@@ -215,25 +215,25 @@ const Fe = /* @__PURE__ */ le({
|
|
|
215
215
|
}
|
|
216
216
|
] : []
|
|
217
217
|
);
|
|
218
|
-
return (o, h) => (
|
|
218
|
+
return (o, h) => (c(), $("div", {
|
|
219
219
|
class: "wx-review",
|
|
220
|
-
onKeydown:
|
|
220
|
+
onKeydown: J
|
|
221
221
|
}, [
|
|
222
|
-
q.value ? (
|
|
222
|
+
q.value ? (c(), x(t(ve), {
|
|
223
223
|
key: 0,
|
|
224
224
|
class: "wx-review__ghost",
|
|
225
225
|
title: "",
|
|
226
226
|
rows: 6
|
|
227
|
-
})) : (
|
|
228
|
-
f(t(
|
|
227
|
+
})) : (c(), $(Q, { key: 1 }, [
|
|
228
|
+
f(t(me), {
|
|
229
229
|
level: 3,
|
|
230
230
|
back: !s.inline,
|
|
231
231
|
"back-label": t(l)("review.back"),
|
|
232
232
|
title: H.value,
|
|
233
|
-
actions:
|
|
233
|
+
actions: K.value,
|
|
234
234
|
"collapse-below": 480,
|
|
235
235
|
onBack: h[0] || (h[0] = (I) => a("back"))
|
|
236
|
-
},
|
|
236
|
+
}, Re({ _: 2 }, [
|
|
237
237
|
k.value && !k.value.published ? {
|
|
238
238
|
name: "title-after",
|
|
239
239
|
fn: p(() => [
|
|
@@ -247,19 +247,19 @@ const Fe = /* @__PURE__ */ le({
|
|
|
247
247
|
key: "0"
|
|
248
248
|
} : void 0
|
|
249
249
|
]), 1032, ["back", "back-label", "title", "actions"]),
|
|
250
|
-
f(t(
|
|
250
|
+
f(t(be), {
|
|
251
251
|
modelValue: g.value,
|
|
252
252
|
"onUpdate:modelValue": h[1] || (h[1] = (I) => g.value = I),
|
|
253
253
|
name: "reviews.form",
|
|
254
254
|
errors: T.value,
|
|
255
255
|
disabled: !b.value || d.value
|
|
256
256
|
}, null, 8, ["modelValue", "errors", "disabled"]),
|
|
257
|
-
b.value ? (
|
|
257
|
+
b.value ? (c(), x(t(Ae), { key: 0 }, {
|
|
258
258
|
state: p(() => [
|
|
259
|
-
f(t(
|
|
259
|
+
f(t(xe), { state: O.value }, null, 8, ["state"])
|
|
260
260
|
]),
|
|
261
261
|
default: p(() => [
|
|
262
|
-
f(t(
|
|
262
|
+
f(t(ze), {
|
|
263
263
|
type: "primary",
|
|
264
264
|
loading: d.value,
|
|
265
265
|
disabled: !m.value && s.id !== null,
|
|
@@ -278,20 +278,20 @@ const Fe = /* @__PURE__ */ le({
|
|
|
278
278
|
}
|
|
279
279
|
}), de = (n, v) => {
|
|
280
280
|
const s = n.__vccOpts || n;
|
|
281
|
-
for (const [a,
|
|
282
|
-
s[a] =
|
|
281
|
+
for (const [a, u] of v)
|
|
282
|
+
s[a] = u;
|
|
283
283
|
return s;
|
|
284
|
-
}, He = /* @__PURE__ */ de(Fe, [["__scopeId", "data-v-
|
|
284
|
+
}, He = /* @__PURE__ */ de(Fe, [["__scopeId", "data-v-26abe508"]]), Ge = { class: "wx-reviews__bar" }, Qe = { class: "wx-review-row__body" }, Xe = { class: "wx-review-row__who" }, Ye = { class: "wx-review-row__name" }, Ze = ["aria-label"], et = {
|
|
285
285
|
key: 0,
|
|
286
286
|
class: "wx-review-row__job"
|
|
287
|
-
},
|
|
287
|
+
}, tt = { class: "wx-review-row__facts" }, at = ["aria-label"], rt = /* @__PURE__ */ le({
|
|
288
288
|
__name: "ReviewsPage",
|
|
289
289
|
props: {
|
|
290
290
|
base: { default: "/reviews" }
|
|
291
291
|
},
|
|
292
292
|
setup(n) {
|
|
293
|
-
const v = Z(), s =
|
|
294
|
-
|
|
293
|
+
const v = Z(), s = ue(v), a = Be(), u = Ve();
|
|
294
|
+
ce();
|
|
295
295
|
const i = oe("webx-reviews"), B = se(), l = W([]), V = W([]), k = W(!0), g = y(() => v.can("reviews.manage")), C = y(
|
|
296
296
|
() => v.state.manifest?.modules.find((e) => e.id === "reviews")?.title ?? i("module.reviews")
|
|
297
297
|
), q = y({
|
|
@@ -301,7 +301,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
301
301
|
...a.query,
|
|
302
302
|
view: e === "" ? void 0 : String(e)
|
|
303
303
|
};
|
|
304
|
-
delete w.review,
|
|
304
|
+
delete w.review, u.replace({ query: w });
|
|
305
305
|
}
|
|
306
306
|
}), d = y(() => q.value === "trashed"), T = y(() => {
|
|
307
307
|
const e = a.query.category;
|
|
@@ -312,12 +312,12 @@ const Fe = /* @__PURE__ */ le({
|
|
|
312
312
|
}), O = y({
|
|
313
313
|
get: () => m.value !== null,
|
|
314
314
|
set: (e) => {
|
|
315
|
-
e ||
|
|
315
|
+
e || K();
|
|
316
316
|
}
|
|
317
317
|
}), H = y(() => [
|
|
318
318
|
{ value: "", label: i("review.all") },
|
|
319
319
|
{ value: "trashed", label: i("review.trashed"), icon: "trash" }
|
|
320
|
-
]), P =
|
|
320
|
+
]), P = _e(Y, () => ({
|
|
321
321
|
q: b.value,
|
|
322
322
|
category: T.value,
|
|
323
323
|
filtered: d.value
|
|
@@ -346,20 +346,20 @@ const Fe = /* @__PURE__ */ le({
|
|
|
346
346
|
let A;
|
|
347
347
|
te(L, (e) => {
|
|
348
348
|
clearTimeout(A), A = setTimeout(() => {
|
|
349
|
-
|
|
349
|
+
u.replace({ query: { ...a.query, q: e.trim() === "" ? void 0 : e } });
|
|
350
350
|
}, 300);
|
|
351
351
|
}), ne(() => clearTimeout(A));
|
|
352
|
-
function
|
|
353
|
-
|
|
352
|
+
function J(e) {
|
|
353
|
+
u.replace({
|
|
354
354
|
query: { ...a.query, category: typeof e == "number" ? String(e) : void 0 }
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
function z(e) {
|
|
358
|
-
|
|
358
|
+
u.replace({ query: { ...a.query, review: String(e) } });
|
|
359
359
|
}
|
|
360
|
-
function
|
|
360
|
+
function K() {
|
|
361
361
|
const e = { ...a.query };
|
|
362
|
-
delete e.review,
|
|
362
|
+
delete e.review, u.replace({ query: e });
|
|
363
363
|
}
|
|
364
364
|
function o(e) {
|
|
365
365
|
z(e.id), R();
|
|
@@ -408,7 +408,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
function ee(e) {
|
|
411
|
-
m.value === e &&
|
|
411
|
+
m.value === e && K(), R();
|
|
412
412
|
}
|
|
413
413
|
async function he(e) {
|
|
414
414
|
try {
|
|
@@ -426,7 +426,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
426
426
|
const w = Math.max(0, Math.min(5, Math.round(e)));
|
|
427
427
|
return "★".repeat(w) + "☆".repeat(5 - w);
|
|
428
428
|
}
|
|
429
|
-
return (e, w) => (
|
|
429
|
+
return (e, w) => (c(), x(t(ke), {
|
|
430
430
|
view: q.value,
|
|
431
431
|
"onUpdate:view": w[4] || (w[4] = (r) => q.value = r),
|
|
432
432
|
title: C.value,
|
|
@@ -434,12 +434,12 @@ const Fe = /* @__PURE__ */ le({
|
|
|
434
434
|
card: !1
|
|
435
435
|
}, {
|
|
436
436
|
default: p(() => [
|
|
437
|
-
f(t(
|
|
437
|
+
f(t(Ie), {
|
|
438
438
|
class: "wx-reviews",
|
|
439
439
|
padding: "none"
|
|
440
440
|
}, {
|
|
441
441
|
default: p(() => [
|
|
442
|
-
f(t(
|
|
442
|
+
f(t(Me), {
|
|
443
443
|
open: O.value,
|
|
444
444
|
"onUpdate:open": w[3] || (w[3] = (r) => O.value = r),
|
|
445
445
|
class: "wx-reviews__panes",
|
|
@@ -448,8 +448,8 @@ const Fe = /* @__PURE__ */ le({
|
|
|
448
448
|
"detail-label": t(i)("module.reviews")
|
|
449
449
|
}, {
|
|
450
450
|
list: p(() => [
|
|
451
|
-
D("div",
|
|
452
|
-
f(t(
|
|
451
|
+
D("div", Ge, [
|
|
452
|
+
f(t(Ue), {
|
|
453
453
|
modelValue: L.value,
|
|
454
454
|
"onUpdate:modelValue": w[0] || (w[0] = (r) => L.value = r),
|
|
455
455
|
class: "wx-reviews__search",
|
|
@@ -466,7 +466,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
466
466
|
]),
|
|
467
467
|
_: 1
|
|
468
468
|
}, 8, ["modelValue", "placeholder", "aria-label"]),
|
|
469
|
-
d.value ? N("", !0) : (
|
|
469
|
+
d.value ? N("", !0) : (c(), x(t(De), {
|
|
470
470
|
key: 0,
|
|
471
471
|
"model-value": T.value,
|
|
472
472
|
options: V.value.map((r) => ({ value: r.id, label: r.title })),
|
|
@@ -474,10 +474,10 @@ const Fe = /* @__PURE__ */ le({
|
|
|
474
474
|
"aria-label": t(i)("review.filter-category"),
|
|
475
475
|
clearable: "",
|
|
476
476
|
size: "sm",
|
|
477
|
-
"onUpdate:modelValue":
|
|
477
|
+
"onUpdate:modelValue": J
|
|
478
478
|
}, null, 8, ["model-value", "options", "placeholder", "aria-label"]))
|
|
479
479
|
]),
|
|
480
|
-
g.value && !d.value ? (
|
|
480
|
+
g.value && !d.value ? (c(), $("button", {
|
|
481
481
|
key: 0,
|
|
482
482
|
type: "button",
|
|
483
483
|
class: G(["wx-reviews__new", { "is-current": m.value === "new" }]),
|
|
@@ -489,16 +489,16 @@ const Fe = /* @__PURE__ */ le({
|
|
|
489
489
|
}),
|
|
490
490
|
M(" " + S(t(i)("review.new")), 1)
|
|
491
491
|
], 2)) : N("", !0),
|
|
492
|
-
k.value && l.value.length === 0 ? (
|
|
492
|
+
k.value && l.value.length === 0 ? (c(), x(t(ve), {
|
|
493
493
|
key: 1,
|
|
494
494
|
class: "wx-reviews__loading",
|
|
495
495
|
rows: 5
|
|
496
|
-
})) : l.value.length === 0 ? (
|
|
496
|
+
})) : l.value.length === 0 ? (c(), x(t(ae), {
|
|
497
497
|
key: 2,
|
|
498
498
|
title: E.value,
|
|
499
499
|
description: !d.value && b.value === "" ? t(i)("review.empty-help") : void 0
|
|
500
|
-
}, null, 8, ["title", "description"])) : (
|
|
501
|
-
f(t(
|
|
500
|
+
}, null, 8, ["title", "description"])) : (c(), $(Q, { key: 3 }, [
|
|
501
|
+
f(t(Oe), {
|
|
502
502
|
modelValue: l.value,
|
|
503
503
|
"onUpdate:modelValue": w[2] || (w[2] = (r) => l.value = r),
|
|
504
504
|
class: G(["wx-reviews__list", { "is-loading": k.value }]),
|
|
@@ -510,31 +510,31 @@ const Fe = /* @__PURE__ */ le({
|
|
|
510
510
|
onMove: h
|
|
511
511
|
}, {
|
|
512
512
|
default: p(({ item: r }) => [
|
|
513
|
-
(
|
|
513
|
+
(c(), x(Ne(d.value ? "div" : "button"), {
|
|
514
514
|
type: d.value ? void 0 : "button",
|
|
515
515
|
class: G(["wx-review-row", { "is-current": m.value === r.id }]),
|
|
516
516
|
onClick: (U) => d.value ? void 0 : z(r.id)
|
|
517
517
|
}, {
|
|
518
518
|
default: p(() => [
|
|
519
|
-
f(t(
|
|
519
|
+
f(t(je), {
|
|
520
520
|
class: "wx-review-row__photo",
|
|
521
521
|
src: r.photo?.thumb ?? void 0,
|
|
522
522
|
name: r.name,
|
|
523
523
|
size: "md"
|
|
524
524
|
}, null, 8, ["src", "name"]),
|
|
525
|
-
D("span",
|
|
526
|
-
D("span",
|
|
527
|
-
D("span",
|
|
528
|
-
r.rating !== null ? (
|
|
525
|
+
D("span", Qe, [
|
|
526
|
+
D("span", Xe, [
|
|
527
|
+
D("span", Ye, S(r.name), 1),
|
|
528
|
+
r.rating !== null ? (c(), $("span", {
|
|
529
529
|
key: 0,
|
|
530
530
|
class: "wx-review-row__stars",
|
|
531
531
|
role: "img",
|
|
532
532
|
"aria-label": `${r.rating} / 5`
|
|
533
|
-
}, S(ye(r.rating)), 9,
|
|
533
|
+
}, S(ye(r.rating)), 9, Ze)) : N("", !0)
|
|
534
534
|
]),
|
|
535
|
-
r.job_title ? (
|
|
536
|
-
D("span",
|
|
537
|
-
r.published ? pe(r) ? (
|
|
535
|
+
r.job_title ? (c(), $("span", et, S(r.job_title), 1)) : N("", !0),
|
|
536
|
+
D("span", tt, [
|
|
537
|
+
r.published ? pe(r) ? (c(), x(t(Ee), {
|
|
538
538
|
key: 1,
|
|
539
539
|
content: t(i)("review.visible-nowhere")
|
|
540
540
|
}, {
|
|
@@ -551,7 +551,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
551
551
|
})
|
|
552
552
|
]),
|
|
553
553
|
_: 1
|
|
554
|
-
}, 8, ["content"])) : N("", !0) : (
|
|
554
|
+
}, 8, ["content"])) : N("", !0) : (c(), x(t(F), {
|
|
555
555
|
key: 0,
|
|
556
556
|
dot: "",
|
|
557
557
|
size: "sm"
|
|
@@ -561,7 +561,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
561
561
|
]),
|
|
562
562
|
_: 1
|
|
563
563
|
})),
|
|
564
|
-
(
|
|
564
|
+
(c(!0), $(Q, null, qe(r.categories, (U) => (c(), x(t(F), {
|
|
565
565
|
key: U.id,
|
|
566
566
|
size: "sm",
|
|
567
567
|
round: ""
|
|
@@ -571,11 +571,11 @@ const Fe = /* @__PURE__ */ le({
|
|
|
571
571
|
]),
|
|
572
572
|
_: 2
|
|
573
573
|
}, 1024))), 128)),
|
|
574
|
-
r.locales.length > 0 ? (
|
|
574
|
+
r.locales.length > 0 ? (c(), $("span", {
|
|
575
575
|
key: 2,
|
|
576
576
|
class: "wx-review-row__locales",
|
|
577
577
|
"aria-label": t(i)("review.seen-in", { locales: r.locales.join(", ") })
|
|
578
|
-
}, S(r.locales.join(" · ")), 9,
|
|
578
|
+
}, S(r.locales.join(" · ")), 9, at)) : N("", !0)
|
|
579
579
|
])
|
|
580
580
|
])
|
|
581
581
|
]),
|
|
@@ -583,14 +583,14 @@ const Fe = /* @__PURE__ */ le({
|
|
|
583
583
|
}, 1032, ["type", "class", "onClick"]))
|
|
584
584
|
]),
|
|
585
585
|
actions: p(({ item: r }) => [
|
|
586
|
-
f(t(
|
|
586
|
+
f(t(Se), {
|
|
587
587
|
actions: I(r),
|
|
588
588
|
label: r.name
|
|
589
589
|
}, null, 8, ["actions", "label"])
|
|
590
590
|
]),
|
|
591
591
|
_: 1
|
|
592
592
|
}, 8, ["modelValue", "class", "handle", "item-label", "disabled"]),
|
|
593
|
-
g.value && !d.value ? (
|
|
593
|
+
g.value && !d.value ? (c(), x(t(Je), {
|
|
594
594
|
key: 0,
|
|
595
595
|
class: "wx-reviews__note",
|
|
596
596
|
type: "info",
|
|
@@ -599,7 +599,7 @@ const Fe = /* @__PURE__ */ le({
|
|
|
599
599
|
], 64))
|
|
600
600
|
]),
|
|
601
601
|
detail: p(({ inline: r, back: U }) => [
|
|
602
|
-
m.value !== null ? (
|
|
602
|
+
m.value !== null ? (c(), x(He, {
|
|
603
603
|
id: m.value === "new" ? null : m.value,
|
|
604
604
|
key: String(m.value),
|
|
605
605
|
category: T.value,
|
|
@@ -625,8 +625,8 @@ const Fe = /* @__PURE__ */ le({
|
|
|
625
625
|
_: 1
|
|
626
626
|
}, 8, ["view", "title", "views"]));
|
|
627
627
|
}
|
|
628
|
-
}),
|
|
629
|
-
function
|
|
628
|
+
}), it = /* @__PURE__ */ de(rt, [["__scopeId", "data-v-572ff29d"]]);
|
|
629
|
+
function ot(n = "/reviews") {
|
|
630
630
|
return {
|
|
631
631
|
api: "reviews/categories",
|
|
632
632
|
path: `${n}/categories`,
|
|
@@ -653,7 +653,7 @@ function it(n = "/reviews") {
|
|
|
653
653
|
}
|
|
654
654
|
};
|
|
655
655
|
}
|
|
656
|
-
function
|
|
656
|
+
function ut(n = {}) {
|
|
657
657
|
const v = n.path ?? "/reviews";
|
|
658
658
|
return [
|
|
659
659
|
{
|
|
@@ -661,22 +661,22 @@ function vt(n = {}) {
|
|
|
661
661
|
path: v,
|
|
662
662
|
// One route: the open review is in the address (`?review=`), beside the list it was opened
|
|
663
663
|
// from, so a link to it is a link to both.
|
|
664
|
-
routes: [{ path: v, name: "webx.reviews", component:
|
|
664
|
+
routes: [{ path: v, name: "webx.reviews", component: it, props: { base: v } }]
|
|
665
665
|
},
|
|
666
666
|
{
|
|
667
667
|
id: "review-categories",
|
|
668
668
|
path: `${v}/categories`,
|
|
669
|
-
routes:
|
|
669
|
+
routes: We(ot(v))
|
|
670
670
|
}
|
|
671
671
|
];
|
|
672
672
|
}
|
|
673
673
|
export {
|
|
674
674
|
Y as REVIEWS_API,
|
|
675
675
|
He as WxReviewPane,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
676
|
+
it as WxReviewsPage,
|
|
677
|
+
ue as createReviewsApi,
|
|
678
|
+
ot as reviewCategoriesOptions,
|
|
679
|
+
ut as reviews,
|
|
680
|
+
Ke as reviewsMessages
|
|
681
681
|
};
|
|
682
682
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/messages.ts","../src/i18n.ts","../src/ReviewPane.vue","../src/ReviewsPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport type { ReviewDetail, ReviewQuery, ReviewRow, ReviewsList } from './types'\n\nexport interface ReviewsApi {\n /** Every review, with the categories the list can be narrowed to. */\n list(query?: ReviewQuery): Promise<ReviewsList>\n get(id: number): Promise<ReviewDetail>\n /**\n * A new review out of the values of its form, in one request: the form is filled in first and\n * the record made on the first save, so nobody leaves an empty review behind.\n */\n create(values: ScreenModel): Promise<ReviewDetail>\n /** Refused with a 422 under the name of the field. */\n save(id: number, values: ScreenModel): Promise<ReviewDetail>\n remove(id: number): Promise<void>\n restore(id: number): Promise<ReviewRow>\n /** The order on screen: of the whole list, or — with `category` — of that category alone. */\n reorder(ids: number[], category?: number | null): Promise<void>\n}\n\n/** The path of the list under the panel's API, and of its order (`CategoryRoutes::items()`). */\nexport const REVIEWS_API = 'reviews'\n\n/**\n * Everything under `/reviews`, below the panel's API path — except the categories, which are the\n * panel's shared ones and are asked for with `createCategoriesApi(admin, 'reviews/categories')`.\n */\nexport function createReviewsApi(admin: AdminContext): ReviewsApi {\n const base = `${admin.apiPath}/${REVIEWS_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the reviews of no category.\n if (query.search) search.set('search', query.search)\n if (query.category != null) search.set('category', String(query.category))\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<ReviewsList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: ReviewDetail }>(`${base}/${id}`).then(data),\n create: (values) => admin.http.post<{ data: ReviewDetail }>(base, { values }).then(data),\n save: (id, values) =>\n admin.http.put<{ data: ReviewDetail }>(`${base}/${id}`, { values }).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n // The resource of the list line, and not the form's pair: a restored review stays closed.\n restore: (id) => admin.http.post<{ data: ReviewRow }>(`${base}/${id}/restore`, {}).then(data),\n reorder: (ids, category = null) => reorderItems(admin, REVIEWS_API, ids, category),\n }\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-reviews::` and overrides\n * these; `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const reviewsMessages: Record<string, Messages> = {\n module: {\n group: 'Reviews',\n reviews: 'Reviews',\n categories: 'Categories',\n },\n review: {\n new: 'New review',\n untitled: 'Untitled',\n search: 'Search the reviews',\n empty: 'No reviews yet.',\n 'empty-help': 'A review reaches the site in a reviews block, on any page.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n all: 'All',\n trashed: 'Bin',\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n 'not-published': 'Not published',\n 'visible-nowhere': 'Published, but seen in no language: it needs a text in one.',\n 'no-text': 'No text yet',\n 'no-rating': 'No rating',\n 'seen-in': 'Seen in: :locales',\n choose: 'Choose a review',\n 'choose-help': 'Or write a new one: the first line of the list.',\n save: 'Save',\n saved: 'The review is saved.',\n 'save-failed': 'The review was not saved.',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete the review by “:name”?',\n 'delete-text':\n 'It goes to the bin and leaves every block on the site. Restored, it comes back to its places.',\n deleted: 'The review is in the bin.',\n restore: 'Restore',\n restored: 'The review is back.',\n 'reorder-failed': 'The new order was not saved.',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'What you wrote in this review is not on the server.',\n leave: 'Leave',\n back: 'Back to the list',\n },\n screen: {\n author: 'Who wrote it',\n photo: 'Photo',\n 'photo-help': 'Without one, the site shows the initials of the name.',\n name: 'Name',\n 'name-help': 'Not written in a language, it is shown as written in the default one.',\n 'job-title': 'Job title',\n 'job-title-help': 'Or the company, or the city: whatever is printed under the name.',\n 'profile-url': 'Link to a profile',\n 'profile-url-help':\n 'An address starting with http:// or https://. The name becomes a link to it.',\n review: 'Review',\n text: 'Text',\n 'text-help': 'A review is shown in a language only when it has a text in it.',\n rating: 'Rating',\n 'rating-help': 'From one star to five. No stars, no rating shown.',\n 'reviewed-on': 'Date',\n 'reviewed-on-help':\n 'Shown if the block prints it; it changes neither the order nor who sees the review.',\n settings: 'Settings',\n published: 'Published',\n 'published-help':\n 'On the site in every block that shows its categories, from the moment it is saved.',\n categories: 'Categories',\n 'categories-help': 'What a block picks the review by, and the buttons of its filter.',\n },\n category: {\n 'field-title': 'Title',\n visible: 'Shown on the site',\n 'visible-help':\n 'Hidden, it drops out of every filter; its reviews stay in the blocks that show them.',\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help':\n 'A category groups reviews: a block shows the ones you pick, and its filter has a button for each.',\n order: 'Drag to change the order of the filter buttons on the site.',\n hidden: 'Hidden from the site',\n reviews: 'Reviews: :count',\n 'show-reviews': 'Show its reviews',\n 'delete-blocked': 'This category still holds reviews. Move them first.',\n 'delete-text': 'It goes to the bin and its button leaves every filter. Its reviews stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { reviewsMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useReviewsMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-reviews', reviewsMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { onBeforeRouteLeave, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxBadge,\n WxButton,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createReviewsApi } from './api'\nimport { useReviewsMessages } from './i18n'\nimport type { ReviewDetail, ReviewSummary } from './types'\n\n/**\n * One review: the pane beside the list, and the whole screen on a phone.\n *\n * The described screen `reviews.form` with a head over it and one button under it. One explicit\n * save, and no autosave: a review has no draft (decision 10), so what is saved is on the site at\n * once — the button is the moment somebody decides it is ready.\n *\n * A new review is a form first and a record on the first save: an empty row made by pressing\n * \"New review\" and walking away would be a review with nothing in it on the list for ever.\n *\n * The way back is drawn here and not by the pane around it: the drawer a narrow screen opens\n * this in has no close of its own on purpose (CLAUDE.md §4).\n */\ndefineOptions({ name: 'WxReviewPane' })\n\nconst props = withDefaults(\n defineProps<{\n /** `null` — a review not written yet. */\n id: number | null\n /** The category the list is narrowed to: a new review starts in it. */\n category?: number | null\n /** False in the drawer a narrow screen opens this in. */\n inline?: boolean\n }>(),\n { category: null, inline: true },\n)\n\nconst emit = defineEmits<{\n back: []\n /** The review was written; the list redraws its row. */\n saved: [review: ReviewSummary]\n /** The first save of a new review: it is a record now, with an id to be opened by. */\n created: [review: ReviewSummary]\n removed: [id: number]\n}>()\n\nconst context = useAdmin()\nconst api = createReviewsApi(context)\nconst locales = useLocales()\nuseReviewsMessages()\n\nconst t = useTranslate('webx-reviews')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst review = ref<ReviewSummary | null>(null)\nconst values = ref<ScreenModel>({})\nconst snapshot = ref('')\n\nconst loading = ref(props.id !== null)\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst canManage = computed(() => context.can('reviews.manage'))\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value || props.id === null ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: an edit shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.name as LocalizedValue | string | null | undefined\n const fallback = props.id === null ? t('review.new') : (review.value?.name ?? '')\n\n return localizedValue(written, locales.active.value, fallback) || t('review.untitled')\n})\n\nfunction take(detail: ReviewDetail): void {\n review.value = detail.review\n values.value = detail.values\n snapshot.value = JSON.stringify(detail.values)\n}\n\nif (props.id === null) {\n // What the server would answer for a review with nothing in it yet.\n const blank: ScreenModel = {\n name: {},\n job_title: {},\n text: {},\n published: false,\n categories: props.category === null ? [] : [props.category],\n }\n\n values.value = blank\n snapshot.value = JSON.stringify(blank)\n}\n\nasync function load(): Promise<void> {\n if (props.id === null) return\n\n loading.value = true\n\n try {\n take(await api.get(props.id))\n } catch (error) {\n toast.danger(message(error))\n emit('back')\n } finally {\n loading.value = false\n }\n}\n\nasync function save(): Promise<void> {\n if (saving.value || !canManage.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n const made = props.id === null\n const detail = made\n ? await api.create(values.value)\n : await api.save(props.id as number, values.value)\n\n take(detail)\n toast.success(t('review.saved'))\n if (made) emit('created', detail.review)\n else emit('saved', detail.review)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]> } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('review.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\nasync function remove(): Promise<void> {\n const record = review.value\n\n if (!record) return\n\n const agreed = await confirm({\n title: t('review.delete-title', { name: record.name }),\n message: t('review.delete-text'),\n confirmText: t('review.delete'),\n cancelText: t('review.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(record.id)\n toast.success(t('review.deleted'))\n // Nothing left to protect: the record is in the bin.\n snapshot.value = current.value\n emit('removed', record.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\n/* Ctrl+S saves, as it does everywhere else that has a save button. */\nfunction onKeydown(event: KeyboardEvent): void {\n if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {\n event.preventDefault()\n if (dirty.value || props.id === null) void save()\n }\n}\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\nasync function mayLeave(): Promise<boolean> {\n if (!dirty.value || !canManage.value) return true\n\n return await confirm({\n title: t('review.leave-title'),\n message: t('review.leave-text'),\n confirmText: t('review.leave'),\n cancelText: t('review.cancel'),\n tone: 'danger',\n })\n}\n\n/*\n * The list and this form are one route, so another review is an update of it rather than a way\n * out — and so is every keystroke in the search above the list, which goes into the address too.\n * Only a change of the review asks.\n */\nonBeforeRouteUpdate(async (to: RouteLocationNormalized, from: RouteLocationNormalized) =>\n to.query.review === from.query.review ? true : await mayLeave(),\n)\n\nonBeforeRouteLeave(mayLeave)\n\n/* What leads away from the review. On a narrow pane the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value && review.value !== null\n ? [\n {\n key: 'delete',\n label: t('review.delete'),\n icon: 'trash',\n danger: true,\n menu: true,\n run: () => void remove(),\n },\n ]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-review\" @keydown=\"onKeydown\">\n <wx-skeleton v-if=\"loading\" class=\"wx-review__ghost\" title :rows=\"6\" />\n\n <template v-else>\n <wx-screen-head\n :level=\"3\"\n :back=\"!props.inline\"\n :back-label=\"t('review.back')\"\n :title=\"title\"\n :actions=\"actions\"\n :collapse-below=\"480\"\n @back=\"emit('back')\"\n >\n <template v-if=\"review && !review.published\" #title-after>\n <wx-badge dot>{{ t('review.not-published') }}</wx-badge>\n </template>\n </wx-screen-head>\n\n <wx-screen\n v-model=\"values\"\n name=\"reviews.form\"\n :errors=\"errors\"\n :disabled=\"!canManage || saving\"\n />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button\n type=\"primary\"\n :loading=\"saving\"\n :disabled=\"!dirty && props.id !== null\"\n @click=\"save\"\n >\n {{ t('review.save') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-review {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-width: 0;\n padding: var(--wx-gap, var(--wx-space-16));\n container-type: inline-size;\n}\n\n.wx-review__ghost {\n max-width: 480px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n confirm,\n toast,\n WxAlert,\n WxAvatar,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useItemOrder,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n} from '@webx-ui/module-admin'\nimport { createReviewsApi, REVIEWS_API } from './api'\nimport { useReviewsMessages } from './i18n'\nimport ReviewPane from './ReviewPane.vue'\nimport type { ReviewCategoryRef, ReviewRow, ReviewSummary } from './types'\n\n/**\n * The reviews: all of them on the left, the one being written on the right (§4.6, decision 11).\n *\n * The same shape as the FAQ, for the same reason: a review is typed in from a message in a\n * minute, and the next one is usually the one under it — a round trip through a page of its own\n * for every review is most of the time spent. On a phone the form is the whole screen, in the\n * drawer `WxListDetail` raises, and draws its own way back.\n *\n * The order is decision 6: without a filter a drag moves the common order; narrowed to one\n * category, the category's own; a search or the bin shows a selection, so the grips go away and\n * the line under the list says why.\n *\n * Everything the screen is looking at lives in the address — the view, the category, the search\n * and the open review — so a link to a review is a link to it in its list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/reviews' })\n\nconst context = useAdmin()\nconst api = createReviewsApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseReviewsMessages()\n\nconst t = useTranslate('webx-reviews')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<ReviewRow[]>([])\nconst categories = ref<ReviewCategoryRef[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('reviews.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'reviews')?.title ??\n t('module.reviews'),\n)\n\nconst view = computed<TabValue>({\n get: () => (route.query.view === 'trashed' ? 'trashed' : ''),\n set: (value) => {\n const query: LocationQueryRaw = {\n ...route.query,\n view: value === '' ? undefined : String(value),\n }\n\n // A review from the list is not a review from the bin: the form closes with the view.\n delete query.review\n void router.replace({ query })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst category = computed<number | null>(() => {\n const raw = route.query.category\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\n/** The open review: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.review\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/*\n * Open is \"a review is chosen\", and closing is un-choosing it — so the address, the list's\n * highlight and the drawer on a phone can never disagree about it.\n */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('review.all') },\n { value: 'trashed', label: t('review.trashed'), icon: 'trash' },\n])\n\nconst order = useItemOrder(REVIEWS_API, () => ({\n q: q.value,\n category: category.value,\n filtered: inBin.value,\n}))\n\n/* Dragged only by somebody who may write, and only in a list that is a whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('review.empty-search')\n\n return inBin.value ? t('review.empty-bin') : t('review.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n search: q.value,\n category: inBin.value ? null : category.value,\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n categories.value = answer.filters.categories\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(value: unknown): void {\n void router.replace({\n query: { ...route.query, category: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\n/* `replace` and not `push`: going through twenty reviews is not twenty steps of history. */\nfunction choose(id: number | 'new'): void {\n void router.replace({ query: { ...route.query, review: String(id) } })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.review\n void router.replace({ query })\n}\n\n/** A new review was saved: it is a record now, and the list has it. */\nfunction created(review: ReviewSummary): void {\n choose(review.id)\n void load()\n}\n\n/**\n * The order on screen, written as a whole — the list or the category's, whichever this is. The\n * list is already in its new order (`WxSortableList` reorders the model before it says so), so a\n * failure has to put it back rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await order.move(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('review.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(row: ReviewRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('review.restore'),\n run: () => void restore(row),\n },\n ]\n }\n\n return [\n {\n key: 'delete',\n icon: 'trash',\n label: t('review.delete'),\n danger: true,\n run: () => void remove(row),\n },\n ]\n}\n\nasync function remove(row: ReviewRow): Promise<void> {\n const agreed = await confirm({\n title: t('review.delete-title', { name: row.name }),\n message: t('review.delete-text'),\n confirmText: t('review.delete'),\n cancelText: t('review.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('review.deleted'))\n removed(row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/** Gone from the list; and if it was the open one, the form goes with it. */\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n\nasync function restore(row: ReviewRow): Promise<void> {\n try {\n await api.restore(row.id)\n toast.success(t('review.restored'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\n/* Published and yet shown in no language: the one state the list has to point out. */\nfunction unseen(row: ReviewRow): boolean {\n return row.published && row.locales.length === 0\n}\n\n/* Five places, the rating's filled: text rather than `WxRate`, which is a control, and a control\n inside the row's button is a button in a button. */\nfunction stars(rating: number): string {\n const whole = Math.max(0, Math.min(5, Math.round(rating)))\n\n return '★'.repeat(whole) + '☆'.repeat(5 - whole)\n}\n</script>\n\n<template>\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :card=\"false\">\n <wx-card class=\"wx-reviews\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-reviews__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.reviews')\"\n >\n <template #list>\n <div class=\"wx-reviews__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-reviews__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('review.search')\"\n :aria-label=\"t('review.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- Always in sight rather than behind a funnel: it is the one filter that decides\n which order a drag writes, and the list says so right under it. -->\n <wx-select\n v-if=\"!inBin\"\n :model-value=\"category\"\n :options=\"categories.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('review.any-category')\"\n :aria-label=\"t('review.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow\"\n />\n </div>\n\n <!--\n A line of the list rather than a button in the head: the new review is written where\n it will stand, beside the ones it joins — and the form opens the same way any other\n review's does.\n -->\n <button\n v-if=\"canManage && !inBin\"\n type=\"button\"\n class=\"wx-reviews__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('review.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-reviews__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"!inBin && q === '' ? t('review.empty-help') : undefined\"\n />\n\n <template v-else>\n <!-- A grip and not the whole row: the row opens a review, and a row that both opens\n and drags is one where one of the two happens by accident. -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-reviews__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: ReviewRow) => item.name\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'button'\"\n :type=\"inBin ? undefined : 'button'\"\n class=\"wx-review-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"inBin ? undefined : choose(item.id)\"\n >\n <wx-avatar\n class=\"wx-review-row__photo\"\n :src=\"item.photo?.thumb ?? undefined\"\n :name=\"item.name\"\n size=\"md\"\n />\n\n <span class=\"wx-review-row__body\">\n <span class=\"wx-review-row__who\">\n <span class=\"wx-review-row__name\">{{ item.name }}</span>\n <span\n v-if=\"item.rating !== null\"\n class=\"wx-review-row__stars\"\n role=\"img\"\n :aria-label=\"`${item.rating} / 5`\"\n >\n {{ stars(item.rating) }}\n </span>\n </span>\n\n <span v-if=\"item.job_title\" class=\"wx-review-row__job\">\n {{ item.job_title }}\n </span>\n\n <span class=\"wx-review-row__facts\">\n <wx-badge v-if=\"!item.published\" dot size=\"sm\">\n {{ t('review.not-published') }}\n </wx-badge>\n <wx-tooltip v-else-if=\"unseen(item)\" :content=\"t('review.visible-nowhere')\">\n <wx-badge type=\"warning\" dot size=\"sm\">\n {{ t('review.not-published') }}\n </wx-badge>\n </wx-tooltip>\n\n <wx-badge v-for=\"chip in item.categories\" :key=\"chip.id\" size=\"sm\" round>\n {{ chip.title }}\n </wx-badge>\n\n <span\n v-if=\"item.locales.length > 0\"\n class=\"wx-review-row__locales\"\n :aria-label=\"t('review.seen-in', { locales: item.locales.join(', ') })\"\n >\n {{ item.locales.join(' · ') }}\n </span>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.name\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-reviews__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <review-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :category=\"category\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('review.choose')\"\n :description=\"canManage ? t('review.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, the way it does on every other list of the panel. The card's\n * corners are the screen's, and the panes paint their own background to the edge: `clip`, not\n * `hidden`, which would make this a scroll container and pin everything sticky inside to a box\n * that never moves (CLAUDE.md §4).\n */\n.wx-reviews > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-reviews__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-reviews__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-reviews__bar > :deep(.wx-select) {\n flex: 1 1 140px;\n min-width: 0;\n}\n\n.wx-reviews__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-reviews__new:hover,\n.wx-reviews__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-reviews__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-reviews__list {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-reviews__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-reviews__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-4);\n padding-inline: var(--wx-space-4);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-reviews__list :deep(.wx-sortable-list__row:hover),\n.wx-reviews__list :deep(.wx-sortable-list__row:has(.is-current)) {\n background: var(--wx-bg-subtle);\n}\n\n.wx-reviews__note {\n margin: var(--wx-space-8);\n}\n\n.wx-review-row {\n display: flex;\n align-items: flex-start;\n gap: var(--wx-space-10);\n /* The row is the target: a short name should not leave half the line dead — and a button,\n unlike a link, is only as wide as what is in it until it is told otherwise. */\n flex: 1 1 auto;\n width: 100%;\n min-width: 0;\n padding-block: var(--wx-space-8);\n padding-inline: 0;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n}\n\nbutton.wx-review-row {\n cursor: pointer;\n}\n\n.wx-review-row__photo {\n flex: none;\n}\n\n.wx-review-row__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--wx-space-2);\n min-width: 0;\n}\n\n.wx-review-row__who {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: var(--wx-space-8);\n min-width: 0;\n}\n\n.wx-review-row__name {\n min-width: 0;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-review-row.is-current .wx-review-row__name {\n color: var(--wx-color-primary);\n}\n\n.wx-review-row__stars {\n flex: none;\n color: var(--wx-color-warning);\n font-size: var(--wx-font-size-xs);\n letter-spacing: 0.08em;\n}\n\n.wx-review-row__job {\n overflow: hidden;\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-review-row__facts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n margin-top: var(--wx-space-2);\n}\n\n.wx-review-row__facts:empty {\n display: none;\n}\n\n.wx-review-row__locales {\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n text-transform: uppercase;\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport ReviewsPage from './ReviewsPage.vue'\n\nexport interface ReviewsOptions {\n /** Where the reviews live inside the panel. The categories sit under it. */\n path?: string\n}\n\n/**\n * The categories of the reviews as the panel's shared category screens see them. Exported so\n * that a panel mounting the screens somewhere of its own does not have to repeat the words.\n *\n * No words about addresses: a category here has none (decision 5) — it is a button of the filter\n * on the site, and the shared list leaves the address line out when the server says so.\n */\nexport function reviewCategoriesOptions(path = '/reviews'): CategoriesOptions {\n return {\n api: 'reviews/categories',\n path: `${path}/categories`,\n name: 'webx.reviews.categories',\n module: 'review-categories',\n screen: 'reviews.category-form',\n manage: 'reviews.categories.manage',\n count: 'reviews_count',\n // The list narrowed to one category is also where its own order is dragged.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-reviews::category.new',\n empty: 'webx-reviews::category.empty',\n 'empty-help': 'webx-reviews::category.empty-help',\n order: 'webx-reviews::category.order',\n hidden: 'webx-reviews::category.hidden',\n count: 'webx-reviews::category.reviews',\n 'show-items': 'webx-reviews::category.show-reviews',\n 'delete-blocked': 'webx-reviews::category.delete-blocked',\n 'delete-text': 'webx-reviews::category.delete-text',\n deleted: 'webx-reviews::category.deleted',\n saved: 'webx-reviews::category.saved',\n 'field-title': 'webx-reviews::category.field-title',\n },\n }\n}\n\n/**\n * The reviews as sections of the panel: reviews, and their categories (§4.6).\n *\n * Two modules rather than one, because the navigation is one entry per module; the server puts\n * both in the `reviews` group, which is what draws them under one heading. A section whose server\n * half is not installed never appears — the entry is built from the manifest.\n */\nexport function reviews(options: ReviewsOptions = {}): AdminModule[] {\n const path = options.path ?? '/reviews'\n\n return [\n {\n id: 'reviews',\n path,\n // One route: the open review is in the address (`?review=`), beside the list it was opened\n // from, so a link to it is a link to both.\n routes: [{ path, name: 'webx.reviews', component: ReviewsPage, props: { base: path } }],\n },\n {\n id: 'review-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(reviewCategoriesOptions(path)),\n },\n ]\n}\n"],"names":["REVIEWS_API","createReviewsApi","admin","base","data","body","query","search","id","values","ids","category","reorderItems","reviewsMessages","seeded","useReviewsMessages","i18n","useAdmin","props","__props","emit","__emit","context","api","locales","useLocales","t","useTranslate","message","useErrorText","review","ref","snapshot","loading","saving","errors","canManage","computed","current","dirty","state","title","written","fallback","localizedValue","take","detail","blank","load","error","toast","save","made","remove","record","confirm","leaveGuard","event","onKeydown","onMounted","onBeforeUnmount","mayLeave","onBeforeRouteUpdate","to","from","onBeforeRouteLeave","actions","_createElementBlock","_createBlock","_unref","WxSkeleton","_Fragment","_createVNode","WxScreenHead","WxBadge","WxScreen","$event","WxActionBar","WxSaveState","WxButton","route","useRoute","router","useRouter","rows","categories","module","view","value","inBin","raw","q","typed","selected","open","close","views","order","useItemOrder","sortable","emptyText","answer","watch","pause","narrow","choose","created","reorder","row","actionsFor","restore","removed","unseen","stars","rating","whole","WxListScreen","WxCard","WxListDetail","_createElementVNode","_hoisted_1","WxInput","WxIcon","WxSelect","item","_normalizeClass","_createTextVNode","WxEmpty","WxSortableList","_withCtx","_openBlock","_resolveDynamicComponent","WxAvatar","_hoisted_2","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","_hoisted_6","_hoisted_7","WxTooltip","_renderList","chip","_hoisted_8","WxRowMenu","WxAlert","inline","back","ReviewPane","reviewCategoriesOptions","path","reviews","options","ReviewsPage","categoryRoutes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/api.ts","../src/messages.ts","../src/i18n.ts","../src/ReviewPane.vue","../src/ReviewsPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport type { ReviewDetail, ReviewQuery, ReviewRow, ReviewsList } from './types'\n\nexport interface ReviewsApi {\n /** Every review, with the categories the list can be narrowed to. */\n list(query?: ReviewQuery): Promise<ReviewsList>\n get(id: number): Promise<ReviewDetail>\n /**\n * A new review out of the values of its form, in one request: the form is filled in first and\n * the record made on the first save, so nobody leaves an empty review behind.\n */\n create(values: ScreenModel): Promise<ReviewDetail>\n /** Refused with a 422 under the name of the field. */\n save(id: number, values: ScreenModel): Promise<ReviewDetail>\n remove(id: number): Promise<void>\n restore(id: number): Promise<ReviewRow>\n /** The order on screen: of the whole list, or — with `category` — of that category alone. */\n reorder(ids: number[], category?: number | null): Promise<void>\n}\n\n/** The path of the list under the panel's API, and of its order (`CategoryRoutes::items()`). */\nexport const REVIEWS_API = 'reviews'\n\n/**\n * Everything under `/reviews`, below the panel's API path — except the categories, which are the\n * panel's shared ones and are asked for with `createCategoriesApi(admin, 'reviews/categories')`.\n */\nexport function createReviewsApi(admin: AdminContext): ReviewsApi {\n const base = `${admin.apiPath}/${REVIEWS_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the reviews of no category.\n if (query.search) search.set('search', query.search)\n if (query.category != null) search.set('category', String(query.category))\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<ReviewsList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: ReviewDetail }>(`${base}/${id}`).then(data),\n create: (values) => admin.http.post<{ data: ReviewDetail }>(base, { values }).then(data),\n save: (id, values) =>\n admin.http.put<{ data: ReviewDetail }>(`${base}/${id}`, { values }).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n // The resource of the list line, and not the form's pair: a restored review stays closed.\n restore: (id) => admin.http.post<{ data: ReviewRow }>(`${base}/${id}/restore`, {}).then(data),\n reorder: (ids, category = null) => reorderItems(admin, REVIEWS_API, ids, category),\n }\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-reviews::` and overrides\n * these; `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const reviewsMessages: Record<string, Messages> = {\n module: {\n group: 'Reviews',\n reviews: 'Reviews',\n categories: 'Categories',\n },\n review: {\n new: 'New review',\n untitled: 'Untitled',\n search: 'Search the reviews',\n empty: 'No reviews yet.',\n 'empty-help': 'A review reaches the site in a reviews block, on any page.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n all: 'All',\n trashed: 'Bin',\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n 'not-published': 'Not published',\n 'visible-nowhere': 'Published, but seen in no language: it needs a text in one.',\n 'no-text': 'No text yet',\n 'no-rating': 'No rating',\n 'seen-in': 'Seen in: :locales',\n choose: 'Choose a review',\n 'choose-help': 'Or write a new one: the first line of the list.',\n save: 'Save',\n saved: 'The review is saved.',\n 'save-failed': 'The review was not saved.',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete the review by “:name”?',\n 'delete-text':\n 'It goes to the bin and leaves every block on the site. Restored, it comes back to its places.',\n deleted: 'The review is in the bin.',\n restore: 'Restore',\n restored: 'The review is back.',\n 'reorder-failed': 'The new order was not saved.',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'What you wrote in this review is not on the server.',\n leave: 'Leave',\n back: 'Back to the list',\n },\n screen: {\n author: 'Who wrote it',\n photo: 'Photo',\n 'photo-help': 'Without one, the site shows the initials of the name.',\n name: 'Name',\n 'name-help': 'Not written in a language, it is shown as written in the default one.',\n 'job-title': 'Job title',\n 'job-title-help': 'Or the company, or the city: whatever is printed under the name.',\n 'profile-url': 'Link to a profile',\n 'profile-url-help':\n 'An address starting with http:// or https://. The name becomes a link to it.',\n review: 'Review',\n text: 'Text',\n 'text-help': 'A review is shown in a language only when it has a text in it.',\n rating: 'Rating',\n 'rating-help': 'From one star to five. No stars, no rating shown.',\n 'reviewed-on': 'Date',\n 'reviewed-on-help':\n 'Shown if the block prints it; it changes neither the order nor who sees the review.',\n settings: 'Settings',\n published: 'Published',\n 'published-help':\n 'On the site in every block that shows its categories, from the moment it is saved.',\n categories: 'Categories',\n 'categories-help': 'What a block picks the review by, and the buttons of its filter.',\n },\n category: {\n 'field-title': 'Title',\n visible: 'Shown on the site',\n 'visible-help':\n 'Hidden, it drops out of every filter; its reviews stay in the blocks that show them.',\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help':\n 'A category groups reviews: a block shows the ones you pick, and its filter has a button for each.',\n order: 'Drag to change the order of the filter buttons on the site.',\n hidden: 'Hidden from the site',\n reviews: 'Reviews: :count',\n 'show-reviews': 'Show its reviews',\n 'delete-blocked': 'This category still holds reviews. Move them first.',\n 'delete-text': 'It goes to the bin and its button leaves every filter. Its reviews stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { reviewsMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useReviewsMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-reviews', reviewsMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { onBeforeRouteLeave, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxBadge,\n WxButton,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n useBodyKeys,\n} from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createReviewsApi } from './api'\nimport { useReviewsMessages } from './i18n'\nimport type { ReviewDetail, ReviewSummary } from './types'\n\n/**\n * One review: the pane beside the list, and the whole screen on a phone.\n *\n * The described screen `reviews.form` with a head over it and one button under it. One explicit\n * save, and no autosave: a review has no draft (decision 10), so what is saved is on the site at\n * once — the button is the moment somebody decides it is ready.\n *\n * A new review is a form first and a record on the first save: an empty row made by pressing\n * \"New review\" and walking away would be a review with nothing in it on the list for ever.\n *\n * The way back is drawn here and not by the pane around it: the drawer a narrow screen opens\n * this in has no close of its own on purpose (CLAUDE.md §4).\n */\ndefineOptions({ name: 'WxReviewPane' })\n\nconst props = withDefaults(\n defineProps<{\n /** `null` — a review not written yet. */\n id: number | null\n /** The category the list is narrowed to: a new review starts in it. */\n category?: number | null\n /** False in the drawer a narrow screen opens this in. */\n inline?: boolean\n }>(),\n { category: null, inline: true },\n)\n\nconst emit = defineEmits<{\n back: []\n /** The review was written; the list redraws its row. */\n saved: [review: ReviewSummary]\n /** The first save of a new review: it is a record now, with an id to be opened by. */\n created: [review: ReviewSummary]\n removed: [id: number]\n}>()\n\nconst context = useAdmin()\nconst api = createReviewsApi(context)\nconst locales = useLocales()\nuseReviewsMessages()\n\nconst t = useTranslate('webx-reviews')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst review = ref<ReviewSummary | null>(null)\nconst values = ref<ScreenModel>({})\nconst snapshot = ref('')\n\nconst loading = ref(props.id !== null)\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst canManage = computed(() => context.can('reviews.manage'))\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value || props.id === null ? 'unsaved' : 'saved'\n})\n\n/** The name follows the field rather than the answer: an edit shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.name as LocalizedValue | string | null | undefined\n const fallback = props.id === null ? t('review.new') : (review.value?.name ?? '')\n\n return localizedValue(written, locales.active.value, fallback) || t('review.untitled')\n})\n\nfunction take(detail: ReviewDetail): void {\n review.value = detail.review\n values.value = detail.values\n snapshot.value = JSON.stringify(detail.values)\n}\n\nif (props.id === null) {\n // What the server would answer for a review with nothing in it yet.\n const blank: ScreenModel = {\n name: {},\n job_title: {},\n text: {},\n published: false,\n categories: props.category === null ? [] : [props.category],\n }\n\n values.value = blank\n snapshot.value = JSON.stringify(blank)\n}\n\nasync function load(): Promise<void> {\n if (props.id === null) return\n\n loading.value = true\n\n try {\n take(await api.get(props.id))\n } catch (error) {\n toast.danger(message(error))\n emit('back')\n } finally {\n loading.value = false\n }\n}\n\nasync function save(): Promise<void> {\n if (saving.value || !canManage.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n const made = props.id === null\n const detail = made\n ? await api.create(values.value)\n : await api.save(props.id as number, values.value)\n\n take(detail)\n toast.success(t('review.saved'))\n if (made) emit('created', detail.review)\n else emit('saved', detail.review)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]> } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('review.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\nasync function remove(): Promise<void> {\n const record = review.value\n\n if (!record) return\n\n const agreed = await confirm({\n title: t('review.delete-title', { name: record.name }),\n message: t('review.delete-text'),\n confirmText: t('review.delete'),\n cancelText: t('review.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(record.id)\n toast.success(t('review.deleted'))\n // Nothing left to protect: the record is in the bin.\n snapshot.value = current.value\n emit('removed', record.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\n/* Ctrl+S saves, as it does everywhere else that has a save button. */\nfunction onKeydown(event: KeyboardEvent): void {\n if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {\n event.preventDefault()\n if (dirty.value || props.id === null) void save()\n }\n}\n\n// And from the page around the form, where a click on empty space leaves the focus.\nuseBodyKeys(onKeydown)\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\nasync function mayLeave(): Promise<boolean> {\n if (!dirty.value || !canManage.value) return true\n\n return await confirm({\n title: t('review.leave-title'),\n message: t('review.leave-text'),\n confirmText: t('review.leave'),\n cancelText: t('review.cancel'),\n tone: 'danger',\n })\n}\n\n/*\n * The list and this form are one route, so another review is an update of it rather than a way\n * out — and so is every keystroke in the search above the list, which goes into the address too.\n * Only a change of the review asks.\n */\nonBeforeRouteUpdate(async (to: RouteLocationNormalized, from: RouteLocationNormalized) =>\n to.query.review === from.query.review ? true : await mayLeave(),\n)\n\nonBeforeRouteLeave(mayLeave)\n\n/* What leads away from the review. On a narrow pane the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value && review.value !== null\n ? [\n {\n key: 'delete',\n label: t('review.delete'),\n icon: 'trash',\n danger: true,\n menu: true,\n run: () => void remove(),\n },\n ]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-review\" @keydown=\"onKeydown\">\n <wx-skeleton v-if=\"loading\" class=\"wx-review__ghost\" title :rows=\"6\" />\n\n <template v-else>\n <wx-screen-head\n :level=\"3\"\n :back=\"!props.inline\"\n :back-label=\"t('review.back')\"\n :title=\"title\"\n :actions=\"actions\"\n :collapse-below=\"480\"\n @back=\"emit('back')\"\n >\n <template v-if=\"review && !review.published\" #title-after>\n <wx-badge dot>{{ t('review.not-published') }}</wx-badge>\n </template>\n </wx-screen-head>\n\n <wx-screen\n v-model=\"values\"\n name=\"reviews.form\"\n :errors=\"errors\"\n :disabled=\"!canManage || saving\"\n />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button\n type=\"primary\"\n :loading=\"saving\"\n :disabled=\"!dirty && props.id !== null\"\n @click=\"save\"\n >\n {{ t('review.save') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-review {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-width: 0;\n padding: var(--wx-gap, var(--wx-space-16));\n container-type: inline-size;\n}\n\n.wx-review__ghost {\n max-width: 480px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n confirm,\n toast,\n WxAlert,\n WxAvatar,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useItemOrder,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n} from '@webx-ui/module-admin'\nimport { createReviewsApi, REVIEWS_API } from './api'\nimport { useReviewsMessages } from './i18n'\nimport ReviewPane from './ReviewPane.vue'\nimport type { ReviewCategoryRef, ReviewRow, ReviewSummary } from './types'\n\n/**\n * The reviews: all of them on the left, the one being written on the right (§4.6, decision 11).\n *\n * The same shape as the FAQ, for the same reason: a review is typed in from a message in a\n * minute, and the next one is usually the one under it — a round trip through a page of its own\n * for every review is most of the time spent. On a phone the form is the whole screen, in the\n * drawer `WxListDetail` raises, and draws its own way back.\n *\n * The order is decision 6: without a filter a drag moves the common order; narrowed to one\n * category, the category's own; a search or the bin shows a selection, so the grips go away and\n * the line under the list says why.\n *\n * Everything the screen is looking at lives in the address — the view, the category, the search\n * and the open review — so a link to a review is a link to it in its list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/reviews' })\n\nconst context = useAdmin()\nconst api = createReviewsApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseReviewsMessages()\n\nconst t = useTranslate('webx-reviews')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<ReviewRow[]>([])\nconst categories = ref<ReviewCategoryRef[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('reviews.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'reviews')?.title ??\n t('module.reviews'),\n)\n\nconst view = computed<TabValue>({\n get: () => (route.query.view === 'trashed' ? 'trashed' : ''),\n set: (value) => {\n const query: LocationQueryRaw = {\n ...route.query,\n view: value === '' ? undefined : String(value),\n }\n\n // A review from the list is not a review from the bin: the form closes with the view.\n delete query.review\n void router.replace({ query })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst category = computed<number | null>(() => {\n const raw = route.query.category\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\n/** The open review: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.review\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/*\n * Open is \"a review is chosen\", and closing is un-choosing it — so the address, the list's\n * highlight and the drawer on a phone can never disagree about it.\n */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('review.all') },\n { value: 'trashed', label: t('review.trashed'), icon: 'trash' },\n])\n\nconst order = useItemOrder(REVIEWS_API, () => ({\n q: q.value,\n category: category.value,\n filtered: inBin.value,\n}))\n\n/* Dragged only by somebody who may write, and only in a list that is a whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('review.empty-search')\n\n return inBin.value ? t('review.empty-bin') : t('review.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n search: q.value,\n category: inBin.value ? null : category.value,\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n categories.value = answer.filters.categories\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(value: unknown): void {\n void router.replace({\n query: { ...route.query, category: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\n/* `replace` and not `push`: going through twenty reviews is not twenty steps of history. */\nfunction choose(id: number | 'new'): void {\n void router.replace({ query: { ...route.query, review: String(id) } })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.review\n void router.replace({ query })\n}\n\n/** A new review was saved: it is a record now, and the list has it. */\nfunction created(review: ReviewSummary): void {\n choose(review.id)\n void load()\n}\n\n/**\n * The order on screen, written as a whole — the list or the category's, whichever this is. The\n * list is already in its new order (`WxSortableList` reorders the model before it says so), so a\n * failure has to put it back rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await order.move(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('review.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(row: ReviewRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('review.restore'),\n run: () => void restore(row),\n },\n ]\n }\n\n return [\n {\n key: 'delete',\n icon: 'trash',\n label: t('review.delete'),\n danger: true,\n run: () => void remove(row),\n },\n ]\n}\n\nasync function remove(row: ReviewRow): Promise<void> {\n const agreed = await confirm({\n title: t('review.delete-title', { name: row.name }),\n message: t('review.delete-text'),\n confirmText: t('review.delete'),\n cancelText: t('review.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('review.deleted'))\n removed(row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/** Gone from the list; and if it was the open one, the form goes with it. */\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n\nasync function restore(row: ReviewRow): Promise<void> {\n try {\n await api.restore(row.id)\n toast.success(t('review.restored'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\n/* Published and yet shown in no language: the one state the list has to point out. */\nfunction unseen(row: ReviewRow): boolean {\n return row.published && row.locales.length === 0\n}\n\n/* Five places, the rating's filled: text rather than `WxRate`, which is a control, and a control\n inside the row's button is a button in a button. */\nfunction stars(rating: number): string {\n const whole = Math.max(0, Math.min(5, Math.round(rating)))\n\n return '★'.repeat(whole) + '☆'.repeat(5 - whole)\n}\n</script>\n\n<template>\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :card=\"false\">\n <wx-card class=\"wx-reviews\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-reviews__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.reviews')\"\n >\n <template #list>\n <div class=\"wx-reviews__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-reviews__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('review.search')\"\n :aria-label=\"t('review.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- Always in sight rather than behind a funnel: it is the one filter that decides\n which order a drag writes, and the list says so right under it. -->\n <wx-select\n v-if=\"!inBin\"\n :model-value=\"category\"\n :options=\"categories.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('review.any-category')\"\n :aria-label=\"t('review.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow\"\n />\n </div>\n\n <!--\n A line of the list rather than a button in the head: the new review is written where\n it will stand, beside the ones it joins — and the form opens the same way any other\n review's does.\n -->\n <button\n v-if=\"canManage && !inBin\"\n type=\"button\"\n class=\"wx-reviews__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('review.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-reviews__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"!inBin && q === '' ? t('review.empty-help') : undefined\"\n />\n\n <template v-else>\n <!-- A grip and not the whole row: the row opens a review, and a row that both opens\n and drags is one where one of the two happens by accident. -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-reviews__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: ReviewRow) => item.name\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'button'\"\n :type=\"inBin ? undefined : 'button'\"\n class=\"wx-review-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"inBin ? undefined : choose(item.id)\"\n >\n <wx-avatar\n class=\"wx-review-row__photo\"\n :src=\"item.photo?.thumb ?? undefined\"\n :name=\"item.name\"\n size=\"md\"\n />\n\n <span class=\"wx-review-row__body\">\n <span class=\"wx-review-row__who\">\n <span class=\"wx-review-row__name\">{{ item.name }}</span>\n <span\n v-if=\"item.rating !== null\"\n class=\"wx-review-row__stars\"\n role=\"img\"\n :aria-label=\"`${item.rating} / 5`\"\n >\n {{ stars(item.rating) }}\n </span>\n </span>\n\n <span v-if=\"item.job_title\" class=\"wx-review-row__job\">\n {{ item.job_title }}\n </span>\n\n <span class=\"wx-review-row__facts\">\n <wx-badge v-if=\"!item.published\" dot size=\"sm\">\n {{ t('review.not-published') }}\n </wx-badge>\n <wx-tooltip v-else-if=\"unseen(item)\" :content=\"t('review.visible-nowhere')\">\n <wx-badge type=\"warning\" dot size=\"sm\">\n {{ t('review.not-published') }}\n </wx-badge>\n </wx-tooltip>\n\n <wx-badge v-for=\"chip in item.categories\" :key=\"chip.id\" size=\"sm\" round>\n {{ chip.title }}\n </wx-badge>\n\n <span\n v-if=\"item.locales.length > 0\"\n class=\"wx-review-row__locales\"\n :aria-label=\"t('review.seen-in', { locales: item.locales.join(', ') })\"\n >\n {{ item.locales.join(' · ') }}\n </span>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.name\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-reviews__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <review-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :category=\"category\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('review.choose')\"\n :description=\"canManage ? t('review.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, the way it does on every other list of the panel. The card's\n * corners are the screen's, and the panes paint their own background to the edge: `clip`, not\n * `hidden`, which would make this a scroll container and pin everything sticky inside to a box\n * that never moves (CLAUDE.md §4).\n */\n.wx-reviews > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-reviews__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-reviews__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-reviews__bar > :deep(.wx-select) {\n flex: 1 1 140px;\n min-width: 0;\n}\n\n.wx-reviews__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-reviews__new:hover,\n.wx-reviews__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-reviews__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-reviews__list {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-reviews__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-reviews__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-4);\n padding-inline: var(--wx-space-4);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-reviews__list :deep(.wx-sortable-list__row:hover),\n.wx-reviews__list :deep(.wx-sortable-list__row:has(.is-current)) {\n background: var(--wx-bg-subtle);\n}\n\n.wx-reviews__note {\n margin: var(--wx-space-8);\n}\n\n.wx-review-row {\n display: flex;\n align-items: flex-start;\n gap: var(--wx-space-10);\n /* The row is the target: a short name should not leave half the line dead — and a button,\n unlike a link, is only as wide as what is in it until it is told otherwise. */\n flex: 1 1 auto;\n width: 100%;\n min-width: 0;\n padding-block: var(--wx-space-8);\n padding-inline: 0;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n}\n\nbutton.wx-review-row {\n cursor: pointer;\n}\n\n.wx-review-row__photo {\n flex: none;\n}\n\n.wx-review-row__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--wx-space-2);\n min-width: 0;\n}\n\n.wx-review-row__who {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: var(--wx-space-8);\n min-width: 0;\n}\n\n.wx-review-row__name {\n min-width: 0;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-review-row.is-current .wx-review-row__name {\n color: var(--wx-color-primary);\n}\n\n.wx-review-row__stars {\n flex: none;\n color: var(--wx-color-warning);\n font-size: var(--wx-font-size-xs);\n letter-spacing: 0.08em;\n}\n\n.wx-review-row__job {\n overflow: hidden;\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-sm);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-review-row__facts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n margin-top: var(--wx-space-2);\n}\n\n.wx-review-row__facts:empty {\n display: none;\n}\n\n.wx-review-row__locales {\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n text-transform: uppercase;\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport ReviewsPage from './ReviewsPage.vue'\n\nexport interface ReviewsOptions {\n /** Where the reviews live inside the panel. The categories sit under it. */\n path?: string\n}\n\n/**\n * The categories of the reviews as the panel's shared category screens see them. Exported so\n * that a panel mounting the screens somewhere of its own does not have to repeat the words.\n *\n * No words about addresses: a category here has none (decision 5) — it is a button of the filter\n * on the site, and the shared list leaves the address line out when the server says so.\n */\nexport function reviewCategoriesOptions(path = '/reviews'): CategoriesOptions {\n return {\n api: 'reviews/categories',\n path: `${path}/categories`,\n name: 'webx.reviews.categories',\n module: 'review-categories',\n screen: 'reviews.category-form',\n manage: 'reviews.categories.manage',\n count: 'reviews_count',\n // The list narrowed to one category is also where its own order is dragged.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-reviews::category.new',\n empty: 'webx-reviews::category.empty',\n 'empty-help': 'webx-reviews::category.empty-help',\n order: 'webx-reviews::category.order',\n hidden: 'webx-reviews::category.hidden',\n count: 'webx-reviews::category.reviews',\n 'show-items': 'webx-reviews::category.show-reviews',\n 'delete-blocked': 'webx-reviews::category.delete-blocked',\n 'delete-text': 'webx-reviews::category.delete-text',\n deleted: 'webx-reviews::category.deleted',\n saved: 'webx-reviews::category.saved',\n 'field-title': 'webx-reviews::category.field-title',\n },\n }\n}\n\n/**\n * The reviews as sections of the panel: reviews, and their categories (§4.6).\n *\n * Two modules rather than one, because the navigation is one entry per module; the server puts\n * both in the `reviews` group, which is what draws them under one heading. A section whose server\n * half is not installed never appears — the entry is built from the manifest.\n */\nexport function reviews(options: ReviewsOptions = {}): AdminModule[] {\n const path = options.path ?? '/reviews'\n\n return [\n {\n id: 'reviews',\n path,\n // One route: the open review is in the address (`?review=`), beside the list it was opened\n // from, so a link to it is a link to both.\n routes: [{ path, name: 'webx.reviews', component: ReviewsPage, props: { base: path } }],\n },\n {\n id: 'review-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(reviewCategoriesOptions(path)),\n },\n ]\n}\n"],"names":["REVIEWS_API","createReviewsApi","admin","base","data","body","query","search","id","values","ids","category","reorderItems","reviewsMessages","seeded","useReviewsMessages","i18n","useAdmin","props","__props","emit","__emit","context","api","locales","useLocales","t","useTranslate","message","useErrorText","review","ref","snapshot","loading","saving","errors","canManage","computed","current","dirty","state","title","written","fallback","localizedValue","take","detail","blank","load","error","toast","save","made","remove","record","confirm","leaveGuard","event","onKeydown","useBodyKeys","onMounted","onBeforeUnmount","mayLeave","onBeforeRouteUpdate","to","from","onBeforeRouteLeave","actions","_createElementBlock","_createBlock","_unref","WxSkeleton","_Fragment","_createVNode","WxScreenHead","WxBadge","WxScreen","$event","WxActionBar","WxSaveState","WxButton","route","useRoute","router","useRouter","rows","categories","module","view","value","inBin","raw","q","typed","selected","open","close","views","order","useItemOrder","sortable","emptyText","answer","watch","pause","narrow","choose","created","reorder","row","actionsFor","restore","removed","unseen","stars","rating","whole","WxListScreen","WxCard","WxListDetail","_createElementVNode","_hoisted_1","WxInput","WxIcon","WxSelect","item","_normalizeClass","_createTextVNode","WxEmpty","WxSortableList","_withCtx","_openBlock","_resolveDynamicComponent","WxAvatar","_hoisted_2","_hoisted_3","_hoisted_4","_toDisplayString","_hoisted_5","_hoisted_6","_hoisted_7","WxTooltip","_renderList","chip","_hoisted_8","WxRowMenu","WxAlert","inline","back","ReviewPane","reviewCategoriesOptions","path","reviews","options","ReviewsPage","categoryRoutes"],"mappings":"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"}
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-review[data-v-
|
|
1
|
+
.wx-review[data-v-26abe508]{display:flex;flex-direction:column;gap:var(--wx-gap, var(--wx-space-16));min-width:0;padding:var(--wx-gap, var(--wx-space-16));container-type:inline-size}.wx-review__ghost[data-v-26abe508]{max-width:480px}.wx-reviews>.wx-card__body{display:flex;flex-direction:column;overflow:clip;border-radius:inherit}.wx-reviews__bar[data-v-572ff29d]{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);padding:var(--wx-space-12) var(--wx-space-12) var(--wx-space-8)}.wx-reviews__search[data-v-572ff29d]{flex:1 1 180px;min-width:0}.wx-reviews__bar[data-v-572ff29d]>.wx-select{flex:1 1 140px;min-width:0}.wx-reviews__new[data-v-572ff29d]{display:flex;align-items:center;gap:var(--wx-space-8);width:calc(100% - 2 * var(--wx-space-8));margin:0 var(--wx-space-8) var(--wx-space-4);padding:var(--wx-space-8);border:1px dashed var(--wx-border-default);border-radius:var(--wx-radius-sm);background:none;font:inherit;color:var(--wx-color-primary);text-align:start;cursor:pointer}.wx-reviews__new[data-v-572ff29d]:hover,.wx-reviews__new.is-current[data-v-572ff29d]{background:var(--wx-bg-subtle)}.wx-reviews__loading[data-v-572ff29d]{padding:var(--wx-space-12)}.wx-reviews__list[data-v-572ff29d]{padding-inline:var(--wx-space-8)}.wx-reviews__list.is-loading[data-v-572ff29d]{opacity:.6}.wx-reviews__list[data-v-572ff29d] .wx-sortable-list__row{gap:var(--wx-space-4);padding-inline:var(--wx-space-4);border-radius:var(--wx-radius-sm)}.wx-reviews__list[data-v-572ff29d] .wx-sortable-list__row:hover,.wx-reviews__list[data-v-572ff29d] .wx-sortable-list__row:has(.is-current){background:var(--wx-bg-subtle)}.wx-reviews__note[data-v-572ff29d]{margin:var(--wx-space-8)}.wx-review-row[data-v-572ff29d]{display:flex;align-items:flex-start;gap:var(--wx-space-10);flex:1 1 auto;width:100%;min-width:0;padding-block:var(--wx-space-8);padding-inline:0;border:0;background:none;font:inherit;color:inherit;text-align:start}button.wx-review-row[data-v-572ff29d]{cursor:pointer}.wx-review-row__photo[data-v-572ff29d]{flex:none}.wx-review-row__body[data-v-572ff29d]{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--wx-space-2);min-width:0}.wx-review-row__who[data-v-572ff29d]{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--wx-space-8);min-width:0}.wx-review-row__name[data-v-572ff29d]{min-width:0;overflow:hidden;font-weight:var(--wx-font-weight-medium);text-overflow:ellipsis;white-space:nowrap}.wx-review-row.is-current .wx-review-row__name[data-v-572ff29d]{color:var(--wx-color-primary)}.wx-review-row__stars[data-v-572ff29d]{flex:none;color:var(--wx-color-warning);font-size:var(--wx-font-size-xs);letter-spacing:.08em}.wx-review-row__job[data-v-572ff29d]{overflow:hidden;color:var(--wx-text-muted);font-size:var(--wx-font-size-sm);text-overflow:ellipsis;white-space:nowrap}.wx-review-row__facts[data-v-572ff29d]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4);min-width:0;margin-top:var(--wx-space-2)}.wx-review-row__facts[data-v-572ff29d]:empty{display:none}.wx-review-row__locales[data-v-572ff29d]{color:var(--wx-text-muted);font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-xs);text-transform:uppercase}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-reviews",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Reviews for the WebX UI admin panel: photo, name, job title, rating and text, in flat categories, ordered by hand, shown on the site by a block.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"vue-router": "^4.5.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@webx-ui/core": "^0.34.
|
|
48
|
-
"@webx-ui/module-admin": "^0.18.
|
|
49
|
-
"@webx-ui/schema": "^0.6.
|
|
47
|
+
"@webx-ui/core": "^0.34.1",
|
|
48
|
+
"@webx-ui/module-admin": "^0.18.2",
|
|
49
|
+
"@webx-ui/schema": "^0.6.4"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^24.10.1",
|