@webority/support-react 0.13.0 → 0.13.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/README.md CHANGED
@@ -42,10 +42,18 @@ export function App() {
42
42
  rejects with a `SupportApiError` carrying `code`, `status` and a `message` written for the
43
43
  person, which the components show as-is.
44
44
 
45
+ `listTickets` returns one page, most recently updated first, in the shape
46
+ `{ items, page, pageSize, totalCount, totalPages }`. Every query part is optional: `status` is one
47
+ of `Open`, `InProgress`, `Resolved` or `Closed`, `search` matches the subject or the ticket
48
+ number, `page` starts at 1, and the API caps `pageSize` at 100. `getTicketCounts` returns
49
+ `{ open, inProgress, resolved, closed, total }`. A ticket's `accountId` and `realm` are `null` on
50
+ the customer side, and its `version` is for support staff only; the components use none of them.
51
+
45
52
  | Method | Request |
46
53
  |---|---|
47
- | `getConfig()` | `GET {base}/config` |
48
- | `listTickets()` | `GET {base}/tickets` |
54
+ | `getConfig()` | `GET {base}/settings` |
55
+ | `listTickets({ status, search, page, pageSize })` | `GET {base}/tickets?status=&search=&page=&pageSize=`, one page |
56
+ | `getTicketCounts()` | `GET {base}/tickets/counts` |
49
57
  | `getTicket(id)` | `GET {base}/tickets/{id}` |
50
58
  | `createTicket({ category, subject, body, pageUrl, userAgent, appVersion, files, screenshots })` | `POST {base}/tickets`, multipart |
51
59
  | `reply(id, { body, files, screenshots })` | `POST {base}/tickets/{id}/replies`, multipart |
@@ -58,7 +66,10 @@ person, which the components show as-is.
58
66
  ### `<SupportCenter />`
59
67
 
60
68
  The support page: the customer's tickets with status tabs and a search box, one ticket's
61
- conversation, and a New ticket dialog.
69
+ conversation, and a New ticket dialog. The server filters, searches and pages the list, so it
70
+ stays quick however many tickets a customer has. The tabs are All, Open, In progress, Resolved
71
+ and Closed, each labelled with its count from `getTicketCounts`. All is chosen first. The search
72
+ asks the server once typing pauses, and a new tab or search starts again at page 1.
62
73
 
63
74
  ```jsx
64
75
  <SupportCenter appVersion={import.meta.env.VITE_APP_VERSION} defaultCategory="Question" />
@@ -74,7 +85,8 @@ Support emails link to the support page with `?ticket=<id>`. The centre opens th
74
85
  keeps the parameter in step as tickets open and close, so a refresh stays on the same ticket.
75
86
  In a ticket the customer can reply with files (pasting an image into the message attaches it),
76
87
  reopen a settled ticket, and say whether it solved their problem. Files are checked against the
77
- config's type, size and count limits before anything uploads.
88
+ config's type, size and count limits before anything uploads, and a file that breaks a limit gets
89
+ one message, in the package's words.
78
90
 
79
91
  ### `<ReportIssue />`
80
92
 
@@ -95,6 +107,17 @@ pasting them. The ticket records the page address, the browser and the product v
95
107
  | `getTicketHref(id)` | Shows a View ticket link to the new ticket. |
96
108
  | `onOpenTicket(ticket)` | Called by the View ticket button when there is no href. |
97
109
  | `onCreated(ticket)` | Called once the ticket is raised. |
110
+ | `className` | Added to the button's wrapper, floating or inline, to place it. |
111
+ | `style` | Merged over the wrapper's own style. |
112
+
113
+ Place the button with `className` or `style`. Both land on the wrapper that holds the button,
114
+ which is the element that floats. With `floating`, an inset moves it clear of other fixed controls
115
+ such as a chat launcher:
116
+
117
+ ```jsx
118
+ <ReportIssue floating style={{ bottom: "5rem" }} />
119
+ <ReportIssue className="ms-auto" />
120
+ ```
98
121
 
99
122
  The screenshot is taken before the dialog opens, and the button itself is left out of it.
100
123
  Anything else the page marks with `data-support-capture="exclude"` is left out too.
@@ -110,8 +133,11 @@ support config. A field the product has not set is left out, and nothing renders
110
133
 
111
134
  ## Hooks
112
135
 
113
- `useSupportConfig()`, `useSupportTickets()` and `useSupportTicket(id)` return the data with
114
- `loading`, `error` and `reload`. `useSupportTicket` also returns `reply`, `reopen` and `rate`,
136
+ `useSupportConfig()`, `useSupportTickets(query)`, `useSupportTicketCounts()` and
137
+ `useSupportTicket(id)` return the data with `loading`, `error` and `reload`.
138
+ `useSupportTickets` takes the same query as `listTickets` and returns `tickets` (the page's items),
139
+ `totalCount` and `totalPages`; it asks again whenever the query changes. `useSupportTicketCounts`
140
+ returns `counts`. `useSupportTicket` also returns `reply`, `reopen` and `rate`,
115
141
  each resolving to the updated ticket. `useSupportClient()` returns the client itself.
116
142
 
117
143
  ## Display rules
@@ -1,14 +1,14 @@
1
1
  import { createContext as e, useCallback as t, useContext as n, useEffect as r, useId as i, useMemo as a, useRef as o, useState as s } from "react";
2
- import { AppAlert as c, AppAvatar as l, AppBadge as u, AppButton as d, AppCard as f, AppCopyButton as p, AppDataTable as m, AppFileAttachment as h, AppIcon as g, AppInfoList as _, AppInput as v, AppLink as y, AppModal as b, AppSearch as x, AppSelect as S, AppSkeleton as C, AppSpinner as w, AppStatusBadge as T, AppTableEmptyState as E, AppTabs as D, AppTextarea as O, usePagination as k } from "@webority/ui-react";
3
- import { toBlob as A } from "html-to-image";
4
- import { Fragment as ee, jsx as j, jsxs as M } from "react/jsx-runtime";
2
+ import { AppAlert as c, AppAvatar as l, AppBadge as u, AppButton as d, AppCard as f, AppCopyButton as p, AppDataTable as m, AppFileAttachment as h, AppIcon as g, AppInfoList as _, AppInput as v, AppLink as y, AppModal as b, AppSearch as x, AppSelect as S, AppSkeleton as C, AppSpinner as w, AppStatusBadge as T, AppTableEmptyState as E, AppTabs as D, AppTextarea as O } from "@webority/ui-react";
3
+ import { toBlob as k } from "html-to-image";
4
+ import { Fragment as A, jsx as j, jsxs as M } from "react/jsx-runtime";
5
5
  //#region src/client.js
6
6
  var N = "Something went wrong. Please try again.", P = class extends Error {
7
7
  constructor(e, t, n) {
8
8
  super(t), this.name = "SupportApiError", this.code = e, this.status = n;
9
9
  }
10
10
  };
11
- async function te(e) {
11
+ async function F(e) {
12
12
  if (e && typeof Blob < "u" && e instanceof Blob) try {
13
13
  return JSON.parse(await e.text());
14
14
  } catch {
@@ -21,63 +21,69 @@ async function te(e) {
21
21
  }
22
22
  return e ?? null;
23
23
  }
24
- async function ne(e) {
24
+ async function I(e) {
25
25
  if (e instanceof P) return e;
26
26
  let t = e?.response;
27
27
  if (!t) return new P("network", N);
28
- let n = await te(t.data);
28
+ let n = await F(t.data);
29
29
  return new P(typeof n?.code == "string" && n.code ? n.code : "unknown", typeof n?.message == "string" && n.message ? n.message : N, t.status);
30
30
  }
31
- async function F(e) {
31
+ async function L(e) {
32
32
  try {
33
33
  return (await e()).data;
34
34
  } catch (e) {
35
- throw await ne(e);
35
+ throw await I(e);
36
36
  }
37
37
  }
38
- function I(e, t, n) {
38
+ function R(e, t, n) {
39
39
  n != null && String(n).trim() !== "" && e.append(t, String(n));
40
40
  }
41
- function L(e, t, n, r) {
41
+ function z(e, t, n, r) {
42
42
  (n ?? []).forEach((n, i) => {
43
43
  let a = n?.name || `${r}-${i + 1}.png`;
44
44
  e.append(t, n, a);
45
45
  });
46
46
  }
47
- function re(e, t = "/api/support") {
47
+ function ee(e, t = "/api/support") {
48
48
  let n = t.replace(/\/+$/, ""), r = (e) => `${n}/tickets/${encodeURIComponent(e)}`;
49
49
  return {
50
- getConfig: () => F(() => e.get(`${n}/config`)),
51
- listTickets: () => F(() => e.get(`${n}/tickets`)),
52
- getTicket: (t) => F(() => e.get(r(t))),
50
+ getConfig: () => L(() => e.get(`${n}/settings`)),
51
+ listTickets: ({ status: t, search: r, page: i, pageSize: a } = {}) => {
52
+ let o = new URLSearchParams();
53
+ R(o, "status", t), R(o, "search", r?.trim()), R(o, "page", i), R(o, "pageSize", a);
54
+ let s = o.toString();
55
+ return L(() => e.get(`${n}/tickets${s ? `?${s}` : ""}`));
56
+ },
57
+ getTicketCounts: () => L(() => e.get(`${n}/tickets/counts`)),
58
+ getTicket: (t) => L(() => e.get(r(t))),
53
59
  createTicket: ({ category: t, subject: r, body: i, pageUrl: a, userAgent: o, appVersion: s, files: c, screenshots: l }) => {
54
60
  let u = new FormData();
55
- return u.append("category", t ?? ""), u.append("subject", r ?? ""), u.append("body", i ?? ""), I(u, "pageUrl", a), I(u, "userAgent", o), I(u, "appVersion", s), L(u, "files", c, "file"), L(u, "screenshots", l, "screenshot"), F(() => e.post(`${n}/tickets`, u));
61
+ return u.append("category", t ?? ""), u.append("subject", r ?? ""), u.append("body", i ?? ""), R(u, "pageUrl", a), R(u, "userAgent", o), R(u, "appVersion", s), z(u, "files", c, "file"), z(u, "screenshots", l, "screenshot"), L(() => e.post(`${n}/tickets`, u));
56
62
  },
57
63
  reply: (t, { body: n, files: i, screenshots: a }) => {
58
64
  let o = new FormData();
59
- return o.append("body", n ?? ""), L(o, "files", i, "file"), L(o, "screenshots", a, "screenshot"), F(() => e.post(`${r(t)}/replies`, o));
65
+ return o.append("body", n ?? ""), z(o, "files", i, "file"), z(o, "screenshots", a, "screenshot"), L(() => e.post(`${r(t)}/replies`, o));
60
66
  },
61
- reopen: (t) => F(() => e.post(`${r(t)}/reopen`)),
67
+ reopen: (t) => L(() => e.post(`${r(t)}/reopen`)),
62
68
  rate: (t, { satisfaction: n, comment: i }) => {
63
69
  let a = { satisfaction: n };
64
- return i != null && i.trim() !== "" && (a.comment = i.trim()), F(() => e.post(`${r(t)}/rating`, a));
70
+ return i != null && i.trim() !== "" && (a.comment = i.trim()), L(() => e.post(`${r(t)}/rating`, a));
65
71
  },
66
- fetchAttachment: (t) => F(() => e.get(`${n}/attachments/${encodeURIComponent(t)}`, { responseType: "blob" }))
72
+ fetchAttachment: (t) => L(() => e.get(`${n}/attachments/${encodeURIComponent(t)}`, { responseType: "blob" }))
67
73
  };
68
74
  }
69
75
  //#endregion
70
76
  //#region src/context.js
71
- var ie = e(null);
72
- function ae() {
73
- let e = n(ie);
77
+ var B = e(null);
78
+ function V() {
79
+ let e = n(B);
74
80
  if (!e) throw Error("Support components and hooks must be used inside <SupportProvider>.");
75
81
  return e;
76
82
  }
77
- function R() {
78
- return ae().client;
83
+ function H() {
84
+ return V().client;
79
85
  }
80
- function z(e, n, i = !0) {
86
+ function U(e, n, i = !0) {
81
87
  let [a, c] = s({
82
88
  data: null,
83
89
  loading: i,
@@ -126,8 +132,8 @@ function z(e, n, i = !0) {
126
132
  setData: m
127
133
  };
128
134
  }
129
- function B() {
130
- let { loadConfig: e } = ae(), { data: t, loading: n, error: r, reload: i } = z(e, "config");
135
+ function W() {
136
+ let { loadConfig: e } = V(), { data: t, loading: n, error: r, reload: i } = U(e, "config");
131
137
  return {
132
138
  config: t,
133
139
  loading: n,
@@ -135,17 +141,39 @@ function B() {
135
141
  reload: i
136
142
  };
137
143
  }
138
- function V() {
139
- let e = R(), { data: t, loading: n, error: r, reload: i } = z(() => e.listTickets(), "tickets");
144
+ function te(e = {}) {
145
+ let t = H(), { status: n, search: r, page: i, pageSize: a } = e, { data: o, loading: s, error: c, reload: l } = U(() => t.listTickets({
146
+ status: n,
147
+ search: r,
148
+ page: i,
149
+ pageSize: a
150
+ }), JSON.stringify([
151
+ "tickets",
152
+ n ?? null,
153
+ r ?? null,
154
+ i ?? null,
155
+ a ?? null
156
+ ]));
140
157
  return {
141
- tickets: t ?? [],
158
+ tickets: o?.items ?? [],
159
+ totalCount: o?.totalCount ?? 0,
160
+ totalPages: o?.totalPages ?? 0,
161
+ loading: s,
162
+ error: c,
163
+ reload: l
164
+ };
165
+ }
166
+ function ne() {
167
+ let e = H(), { data: t, loading: n, error: r, reload: i } = U(() => e.getTicketCounts(), "ticket-counts");
168
+ return {
169
+ counts: t,
142
170
  loading: n,
143
171
  error: r,
144
172
  reload: i
145
173
  };
146
174
  }
147
- function H(e) {
148
- let n = R(), { data: r, loading: i, error: a, reload: o, setData: s } = z(() => n.getTicket(e), e ?? "", !!e), c = t(async (e) => {
175
+ function G(e) {
176
+ let n = H(), { data: r, loading: i, error: a, reload: o, setData: s } = U(() => n.getTicket(e), e ?? "", !!e), c = t(async (e) => {
149
177
  let t = await e();
150
178
  return s(t), t;
151
179
  }, [s]);
@@ -173,7 +201,7 @@ function H(e) {
173
201
  }
174
202
  //#endregion
175
203
  //#region src/lib/format.js
176
- var oe = new Intl.DateTimeFormat(void 0, {
204
+ var re = new Intl.DateTimeFormat(void 0, {
177
205
  day: "numeric",
178
206
  month: "short",
179
207
  year: "numeric",
@@ -181,12 +209,12 @@ var oe = new Intl.DateTimeFormat(void 0, {
181
209
  minute: "2-digit",
182
210
  timeZoneName: "short"
183
211
  });
184
- function U(e) {
212
+ function K(e) {
185
213
  if (!e) return "-";
186
214
  let t = new Date(e);
187
- return Number.isNaN(t.getTime()) ? "-" : oe.format(t);
215
+ return Number.isNaN(t.getTime()) ? "-" : re.format(t);
188
216
  }
189
- function W(e) {
217
+ function q(e) {
190
218
  if (!Number.isFinite(e) || e < 0) return "-";
191
219
  if (e < 1024) return `${e} B`;
192
220
  if (e < 1024 * 1024) return `${Math.round(e / 1024)} KB`;
@@ -195,11 +223,11 @@ function W(e) {
195
223
  }
196
224
  //#endregion
197
225
  //#region src/lib/files.js
198
- function se(e) {
226
+ function ie(e) {
199
227
  let t = e?.allowedContentTypes ?? [];
200
228
  return t.length > 0 ? t.join(",") : void 0;
201
229
  }
202
- function ce(e, t) {
230
+ function ae(e, t) {
203
231
  if (t.length === 0) return !0;
204
232
  let n = (e || "").toLowerCase();
205
233
  return t.some((e) => {
@@ -207,17 +235,17 @@ function ce(e, t) {
207
235
  return t.endsWith("/*") ? n.startsWith(t.slice(0, -1)) : n === t;
208
236
  });
209
237
  }
210
- function G(e, t) {
238
+ function J(e, t) {
211
239
  let n = e ?? [], r = t?.maxAttachmentsPerMessage ?? 0, i = t?.maxAttachmentBytes ?? 0, a = t?.allowedContentTypes ?? [];
212
240
  if (r > 0 && n.length > r) return r === 1 ? "You can attach one file." : `You can attach up to ${r} files.`;
213
241
  for (let e of n) {
214
242
  let t = e.name || "This file";
215
- if (!ce(e.type, a)) return `${t} is a file type we cannot accept.`;
216
- if (i > 0 && e.size > i) return `${t} is larger than ${W(i)}.`;
243
+ if (!ae(e.type, a)) return `${t} is a file type we cannot accept.`;
244
+ if (i > 0 && e.size > i) return `${t} is larger than ${q(i)}.`;
217
245
  }
218
246
  return null;
219
247
  }
220
- function K(e) {
248
+ function Y(e) {
221
249
  let t = Array.from(e.clipboardData?.items ?? []), n = Date.now();
222
250
  return t.filter((e) => e.kind === "file" && e.type.startsWith("image/")).map((e) => e.getAsFile()).filter(Boolean).map((e, t) => {
223
251
  let r = (e.type.split("/")[1] || "png").replace("jpeg", "jpg");
@@ -226,7 +254,7 @@ function K(e) {
226
254
  }
227
255
  //#endregion
228
256
  //#region src/components/AttachmentField.jsx
229
- function q({ id: e, label: t = "Attachments", files: n, onChange: r, config: i, error: a }) {
257
+ function X({ id: e, label: t = "Attachments", files: n, onChange: r, config: i, error: a }) {
230
258
  let o = i?.maxAttachmentBytes ?? 0, s = o > 0 ? Math.round(o / (1024 * 1024) * 10) / 10 : void 0;
231
259
  return /* @__PURE__ */ j(h, {
232
260
  id: e,
@@ -235,30 +263,31 @@ function q({ id: e, label: t = "Attachments", files: n, onChange: r, config: i,
235
263
  multiple: !0,
236
264
  value: n,
237
265
  onChange: r,
238
- accept: se(i),
266
+ accept: ie(i),
239
267
  maxSizeMb: s,
268
+ validate: !1,
240
269
  error: a || void 0
241
270
  });
242
271
  }
243
272
  //#endregion
244
273
  //#region src/components/TicketForm.jsx
245
- function le(e, t) {
274
+ function oe(e, t) {
246
275
  return t && e.includes(t) ? t : e[0] ?? "";
247
276
  }
248
- function J({ id: e, config: t, defaultCategory: n, extraFiles: r = [], onSubmit: i, onBusyChange: a, children: o }) {
249
- let l = t?.categories ?? [], [u, d] = s(() => le(l, n)), [f, p] = s(""), [m, h] = s(""), [g, _] = s([]), [y, b] = s({}), [x, C] = s(""), w = (e) => {
277
+ function se({ id: e, config: t, defaultCategory: n, extraFiles: r = [], onSubmit: i, onBusyChange: a, children: o }) {
278
+ let l = t?.categories ?? [], [u, d] = s(() => oe(l, n)), [f, p] = s(""), [m, h] = s(""), [g, _] = s([]), [y, b] = s({}), [x, C] = s(""), w = (e) => {
250
279
  _(e), b((n) => ({
251
280
  ...n,
252
- files: G([...r, ...e], t) ?? void 0
281
+ files: J([...r, ...e], t) ?? void 0
253
282
  }));
254
283
  }, T = (e) => {
255
- let t = K(e);
284
+ let t = Y(e);
256
285
  t.length > 0 && w([...g, ...t]);
257
286
  };
258
287
  function E() {
259
288
  let e = {};
260
289
  u || (e.category = "Choose a category."), f.trim() || (e.subject = "Give your ticket a subject."), m.trim() || (e.body = "Tell us what happened.");
261
- let n = G([...r, ...g], t);
290
+ let n = J([...r, ...g], t);
262
291
  return n && (e.files = n), b(e), Object.keys(e).length === 0;
263
292
  }
264
293
  async function D(e) {
@@ -325,7 +354,7 @@ function J({ id: e, config: t, defaultCategory: n, extraFiles: r = [], onSubmit:
325
354
  error: y.body
326
355
  }),
327
356
  o,
328
- /* @__PURE__ */ j(q, {
357
+ /* @__PURE__ */ j(X, {
329
358
  id: `${e}-files`,
330
359
  files: g,
331
360
  onChange: w,
@@ -337,16 +366,16 @@ function J({ id: e, config: t, defaultCategory: n, extraFiles: r = [], onSubmit:
337
366
  }
338
367
  //#endregion
339
368
  //#region src/ReportIssue.jsx
340
- var ue = "supportCapture", de = { zIndex: "var(--wui-z-fixed)" }, fe = (e) => e?.dataset?.[ue] !== "exclude";
341
- async function pe() {
342
- let e = await A(document.body, {
343
- filter: fe,
369
+ var ce = "supportCapture", le = { zIndex: "var(--wui-z-fixed)" }, ue = (e) => e?.dataset?.[ce] !== "exclude";
370
+ async function de() {
371
+ let e = await k(document.body, {
372
+ filter: ue,
344
373
  pixelRatio: Math.min(window.devicePixelRatio || 1, 2)
345
374
  });
346
375
  if (!e) throw Error("The page capture came back empty.");
347
376
  return new File([e], "screenshot.png", { type: "image/png" });
348
377
  }
349
- function me({ file: e, onRemove: t }) {
378
+ function fe({ file: e, onRemove: t }) {
350
379
  let [n, i] = s(null);
351
380
  return r(() => {
352
381
  let t = URL.createObjectURL(e);
@@ -374,109 +403,112 @@ function me({ file: e, onRemove: t }) {
374
403
  ]
375
404
  });
376
405
  }
377
- function he({ label: e = "Report an issue", floating: t = !1, defaultCategory: n = "Bug report", appVersion: r, getTicketHref: a, onOpenTicket: o, onCreated: l }) {
378
- let u = R(), { config: f, error: p } = B(), m = `support-report-${i().replace(/[^a-zA-Z0-9-]/g, "")}`, [h, g] = s(!1), [_, v] = s(!1), [y, x] = s(null), [S, C] = s(!1), [w, T] = s(!1), [E, D] = s(null);
379
- async function O() {
380
- v(!0), D(null);
406
+ function pe({ label: e = "Report an issue", floating: t = !1, defaultCategory: n = "Bug report", appVersion: r, getTicketHref: a, onOpenTicket: o, onCreated: l, className: u, style: f }) {
407
+ let p = H(), { config: m, error: h } = W(), g = `support-report-${i().replace(/[^a-zA-Z0-9-]/g, "")}`, [_, v] = s(!1), [y, x] = s(!1), [S, C] = s(null), [w, T] = s(!1), [E, D] = s(!1), [O, k] = s(null);
408
+ async function N() {
409
+ x(!0), k(null);
381
410
  try {
382
- x(await pe()), C(!1);
411
+ C(await de()), T(!1);
383
412
  } catch {
384
- x(null), C(!0);
413
+ C(null), T(!0);
385
414
  } finally {
386
- v(!1), g(!0);
415
+ x(!1), v(!0);
387
416
  }
388
417
  }
389
- let k = () => {
390
- w || (g(!1), x(null), D(null));
418
+ let P = () => {
419
+ E || (v(!1), C(null), k(null));
391
420
  };
392
- async function A(e) {
393
- let t = await u.createTicket({
421
+ async function F(e) {
422
+ let t = await p.createTicket({
394
423
  ...e,
395
424
  pageUrl: window.location.href,
396
425
  userAgent: navigator.userAgent,
397
426
  appVersion: r,
398
- screenshots: y ? [y] : []
427
+ screenshots: S ? [S] : []
399
428
  });
400
- D(t), x(null), l?.(t);
429
+ k(t), C(null), l?.(t);
401
430
  }
402
- let N = E && a ? a(E.id) : void 0;
403
- return /* @__PURE__ */ M(ee, { children: [/* @__PURE__ */ j("span", {
431
+ let I = O && a ? a(O.id) : void 0, L = O ? /* @__PURE__ */ M("div", {
432
+ className: "d-flex justify-content-end gap-2 w-100",
433
+ children: [/* @__PURE__ */ j(d, {
434
+ variant: "secondary",
435
+ onClick: P,
436
+ children: "Close"
437
+ }), O && (I || o) ? /* @__PURE__ */ j(d, {
438
+ href: I,
439
+ onClick: I ? void 0 : () => {
440
+ v(!1), k(null), o(O);
441
+ },
442
+ children: "View ticket"
443
+ }) : null]
444
+ }) : /* @__PURE__ */ M("div", {
445
+ className: "d-flex justify-content-end gap-2 w-100",
446
+ children: [/* @__PURE__ */ j(d, {
447
+ variant: "secondary",
448
+ onClick: P,
449
+ disabled: E,
450
+ children: "Cancel"
451
+ }), /* @__PURE__ */ j(d, {
452
+ type: "submit",
453
+ form: g,
454
+ loading: E,
455
+ disabled: !m,
456
+ children: "Send report"
457
+ })]
458
+ });
459
+ return /* @__PURE__ */ M(A, { children: [/* @__PURE__ */ j("span", {
404
460
  "data-support-capture": "exclude",
405
- className: t ? "position-fixed bottom-0 end-0 m-3" : "d-inline-block",
406
- style: t ? de : void 0,
461
+ className: [t ? "position-fixed bottom-0 end-0 m-3" : "d-inline-block", u].filter(Boolean).join(" "),
462
+ style: t ? {
463
+ ...le,
464
+ ...f
465
+ } : f,
407
466
  children: /* @__PURE__ */ j(d, {
408
467
  leftIcon: "bug",
409
468
  variant: t ? "primary" : "outline",
410
- loading: _,
411
- onClick: O,
469
+ loading: y,
470
+ onClick: N,
412
471
  "aria-haspopup": "dialog",
413
472
  children: e
414
473
  })
415
474
  }), /* @__PURE__ */ j(b, {
416
- open: h,
475
+ open: _,
417
476
  size: "lg",
418
477
  title: "Report an issue",
419
478
  subtitle: "Tell us what went wrong. We attach a screenshot of this page to help us see it.",
420
479
  headerIcon: "bug",
421
480
  staticBackdrop: !0,
422
- onClose: k,
423
- footer: E ? /* @__PURE__ */ M("div", {
424
- className: "d-flex justify-content-end gap-2 w-100",
425
- children: [/* @__PURE__ */ j(d, {
426
- variant: "secondary",
427
- onClick: k,
428
- children: "Close"
429
- }), E && (N || o) ? /* @__PURE__ */ j(d, {
430
- href: N,
431
- onClick: N ? void 0 : () => {
432
- g(!1), D(null), o(E);
433
- },
434
- children: "View ticket"
435
- }) : null]
436
- }) : /* @__PURE__ */ M("div", {
437
- className: "d-flex justify-content-end gap-2 w-100",
438
- children: [/* @__PURE__ */ j(d, {
439
- variant: "secondary",
440
- onClick: k,
441
- disabled: w,
442
- children: "Cancel"
443
- }), /* @__PURE__ */ j(d, {
444
- type: "submit",
445
- form: m,
446
- loading: w,
447
- disabled: !f,
448
- children: "Send report"
449
- })]
450
- }),
451
- children: E ? /* @__PURE__ */ j(c, {
481
+ onClose: P,
482
+ footer: L,
483
+ children: O ? /* @__PURE__ */ j(c, {
452
484
  variant: "success",
453
485
  role: "status",
454
- title: `Ticket ${E.number} is with the support team`,
486
+ title: `Ticket ${O.number} is with the support team`,
455
487
  children: "We reply by email. You can follow the ticket on your support page."
456
- }) : f ? /* @__PURE__ */ j(J, {
457
- id: m,
458
- config: f,
488
+ }) : m ? /* @__PURE__ */ j(se, {
489
+ id: g,
490
+ config: m,
459
491
  defaultCategory: n,
460
- extraFiles: y ? [y] : [],
461
- onSubmit: A,
462
- onBusyChange: T,
463
- children: y ? /* @__PURE__ */ j(me, {
464
- file: y,
465
- onRemove: () => x(null)
466
- }) : S ? /* @__PURE__ */ j(c, {
492
+ extraFiles: S ? [S] : [],
493
+ onSubmit: F,
494
+ onBusyChange: D,
495
+ children: S ? /* @__PURE__ */ j(fe, {
496
+ file: S,
497
+ onRemove: () => C(null)
498
+ }) : w ? /* @__PURE__ */ j(c, {
467
499
  variant: "info",
468
500
  children: "We could not take a screenshot of this page. You can attach an image instead."
469
501
  }) : null
470
502
  }) : /* @__PURE__ */ j(c, {
471
- variant: p ? "danger" : "info",
472
- children: p ? p.message || "We could not load the support settings." : "Loading the form."
503
+ variant: h ? "danger" : "info",
504
+ children: h ? h.message || "We could not load the support settings." : "Loading the form."
473
505
  })
474
506
  })] });
475
507
  }
476
508
  //#endregion
477
509
  //#region src/components/NewTicketModal.jsx
478
- function ge({ open: e, onClose: t, onCreated: n, config: r, defaultCategory: a, appVersion: o }) {
479
- let c = R(), l = `support-new-${i().replace(/[^a-zA-Z0-9-]/g, "")}`, [u, f] = s(!1);
510
+ function me({ open: e, onClose: t, onCreated: n, config: r, defaultCategory: a, appVersion: o }) {
511
+ let c = H(), l = `support-new-${i().replace(/[^a-zA-Z0-9-]/g, "")}`, [u, f] = s(!1);
480
512
  async function p(e) {
481
513
  n(await c.createTicket({
482
514
  ...e,
@@ -508,7 +540,7 @@ function ge({ open: e, onClose: t, onCreated: n, config: r, defaultCategory: a,
508
540
  children: "Send ticket"
509
541
  })]
510
542
  }),
511
- children: e && /* @__PURE__ */ j(J, {
543
+ children: e && /* @__PURE__ */ j(se, {
512
544
  id: l,
513
545
  config: r,
514
546
  defaultCategory: a,
@@ -519,23 +551,23 @@ function ge({ open: e, onClose: t, onCreated: n, config: r, defaultCategory: a,
519
551
  }
520
552
  //#endregion
521
553
  //#region src/lib/status.js
522
- var _e = {
554
+ var he = {
523
555
  Open: "warning",
524
556
  InProgress: "info",
525
557
  Resolved: "success",
526
558
  Closed: "neutral"
527
559
  };
528
- function Y(e) {
529
- return _e[e] ?? "neutral";
560
+ function ge(e) {
561
+ return he[e] ?? "neutral";
530
562
  }
531
563
  //#endregion
532
564
  //#region src/components/AttachmentThumb.jsx
533
- var ve = {
565
+ var _e = {
534
566
  width: 140,
535
567
  maxWidth: "100%"
536
- }, ye = { height: 84 };
537
- function be({ attachment: e }) {
538
- let t = R(), n = (e.contentType || "").startsWith("image/"), [i, a] = s(null), [o, c] = s(!1);
568
+ }, ve = { height: 84 };
569
+ function ye({ attachment: e }) {
570
+ let t = H(), n = (e.contentType || "").startsWith("image/"), [i, a] = s(null), [o, c] = s(!1);
539
571
  r(() => {
540
572
  let n = !0, r = null;
541
573
  return a(null), c(!1), t.fetchAttachment(e.id).then((e) => {
@@ -549,12 +581,12 @@ function be({ attachment: e }) {
549
581
  children: [
550
582
  e.fileName,
551
583
  ", ",
552
- W(e.sizeBytes)
584
+ q(e.sizeBytes)
553
585
  ]
554
586
  });
555
587
  return o ? /* @__PURE__ */ M("span", {
556
588
  className: "d-inline-flex flex-column border rounded overflow-hidden",
557
- style: ve,
589
+ style: _e,
558
590
  children: [/* @__PURE__ */ M("span", {
559
591
  className: "d-flex align-items-center justify-content-center gap-1 p-2 wui-text-xs text-body-secondary",
560
592
  children: [/* @__PURE__ */ j(g, {
@@ -564,7 +596,7 @@ function be({ attachment: e }) {
564
596
  }), u]
565
597
  }) : /* @__PURE__ */ M("a", {
566
598
  className: "d-inline-flex flex-column border rounded overflow-hidden text-decoration-none",
567
- style: ve,
599
+ style: _e,
568
600
  href: i ?? void 0,
569
601
  target: "_blank",
570
602
  rel: "noopener noreferrer",
@@ -572,7 +604,7 @@ function be({ attachment: e }) {
572
604
  "aria-busy": !i || void 0,
573
605
  children: [/* @__PURE__ */ j("span", {
574
606
  className: "d-flex align-items-center justify-content-center bg-body-tertiary",
575
- style: ye,
607
+ style: ve,
576
608
  children: i ? n ? /* @__PURE__ */ j("img", {
577
609
  src: i,
578
610
  alt: e.fileName,
@@ -589,11 +621,11 @@ function be({ attachment: e }) {
589
621
  }
590
622
  //#endregion
591
623
  //#region src/components/MessageThread.jsx
592
- var xe = {
624
+ var be = {
593
625
  whiteSpace: "pre-wrap",
594
626
  overflowWrap: "anywhere"
595
627
  };
596
- function Se({ messages: e }) {
628
+ function xe({ messages: e }) {
597
629
  return /* @__PURE__ */ j("ol", {
598
630
  className: "list-unstyled d-flex flex-column gap-3 mb-0",
599
631
  "aria-label": "Conversation",
@@ -601,7 +633,7 @@ function Se({ messages: e }) {
601
633
  let t = /* @__PURE__ */ j("time", {
602
634
  dateTime: e.createdDateTimeUtc,
603
635
  className: "wui-text-xs text-body-secondary text-nowrap",
604
- children: U(e.createdDateTimeUtc)
636
+ children: K(e.createdDateTimeUtc)
605
637
  });
606
638
  if (e.authorKind === "System") return /* @__PURE__ */ M("li", {
607
639
  className: "text-center wui-text-xs text-body-secondary",
@@ -637,12 +669,12 @@ function Se({ messages: e }) {
637
669
  }),
638
670
  /* @__PURE__ */ j("div", {
639
671
  className: "wui-text-sm",
640
- style: xe,
672
+ style: be,
641
673
  children: e.body
642
674
  }),
643
675
  e.attachments?.length > 0 && /* @__PURE__ */ j("div", {
644
676
  className: "d-flex flex-wrap gap-2 mt-2",
645
- children: e.attachments.map((e) => /* @__PURE__ */ j(be, { attachment: e }, e.id))
677
+ children: e.attachments.map((e) => /* @__PURE__ */ j(ye, { attachment: e }, e.id))
646
678
  })
647
679
  ]
648
680
  })]
@@ -652,7 +684,7 @@ function Se({ messages: e }) {
652
684
  }
653
685
  //#endregion
654
686
  //#region src/components/RatingPanel.jsx
655
- function Ce({ satisfaction: e, comment: t, canRate: n = !1, commentMaxLength: r, onRate: a }) {
687
+ function Se({ satisfaction: e, comment: t, canRate: n = !1, commentMaxLength: r, onRate: a }) {
656
688
  let o = i(), [l, u] = s(""), [p, m] = s(null), [h, g] = s("");
657
689
  if (e) return /* @__PURE__ */ M(c, {
658
690
  variant: e === "Satisfied" ? "success" : "info",
@@ -720,11 +752,11 @@ function Ce({ satisfaction: e, comment: t, canRate: n = !1, commentMaxLength: r,
720
752
  }
721
753
  //#endregion
722
754
  //#region src/components/ReplyBox.jsx
723
- function we({ onSend: e, config: t, reopens: n = !1 }) {
755
+ function Ce({ onSend: e, config: t, reopens: n = !1 }) {
724
756
  let r = i(), [a, o] = s(""), [l, u] = s([]), [f, p] = s(""), [m, h] = s(""), [g, _] = s(!1), v = (e) => {
725
- u(e), p(G(e, t) ?? "");
757
+ u(e), p(J(e, t) ?? "");
726
758
  }, y = (e) => {
727
- let t = K(e);
759
+ let t = Y(e);
728
760
  t.length > 0 && v([...l, ...t]);
729
761
  };
730
762
  async function b(n) {
@@ -734,7 +766,7 @@ function we({ onSend: e, config: t, reopens: n = !1 }) {
734
766
  h("Write a message before you send it.");
735
767
  return;
736
768
  }
737
- let i = G(l, t);
769
+ let i = J(l, t);
738
770
  if (i) {
739
771
  p(i);
740
772
  return;
@@ -773,7 +805,7 @@ function we({ onSend: e, config: t, reopens: n = !1 }) {
773
805
  hint: n ? "Sending a reply reopens this ticket. You can paste an image into the message." : "You can paste an image into the message.",
774
806
  required: !0
775
807
  }),
776
- /* @__PURE__ */ j(q, {
808
+ /* @__PURE__ */ j(X, {
777
809
  id: `${r}-files`,
778
810
  files: l,
779
811
  onChange: v,
@@ -794,7 +826,7 @@ function we({ onSend: e, config: t, reopens: n = !1 }) {
794
826
  }
795
827
  //#endregion
796
828
  //#region src/components/TicketDetail.jsx
797
- function X({ label: e, children: t }) {
829
+ function Z({ label: e, children: t }) {
798
830
  return /* @__PURE__ */ M("div", {
799
831
  className: "d-flex flex-column",
800
832
  children: [/* @__PURE__ */ j("span", {
@@ -806,8 +838,8 @@ function X({ label: e, children: t }) {
806
838
  })]
807
839
  });
808
840
  }
809
- function Te({ ticketId: e, onBack: t, onChanged: n }) {
810
- let { config: r } = B(), { ticket: i, loading: a, error: o, reload: l, reply: u, reopen: f, rate: p } = H(e), [m, h] = s(!1), [g, _] = s(""), v = async (e) => {
841
+ function we({ ticketId: e, onBack: t, onChanged: n }) {
842
+ let { config: r } = W(), { ticket: i, loading: a, error: o, reload: l, reply: u, reopen: f, rate: p } = G(e), [m, h] = s(!1), [g, _] = s(""), v = async (e) => {
811
843
  let t = await e();
812
844
  return n?.(), t;
813
845
  };
@@ -863,32 +895,32 @@ function Te({ ticketId: e, onBack: t, onChanged: n }) {
863
895
  children: i.subject
864
896
  }), i.statusLabel && /* @__PURE__ */ j(T, {
865
897
  label: i.statusLabel,
866
- tone: Y(i.status)
898
+ tone: ge(i.status)
867
899
  })]
868
900
  }),
869
901
  /* @__PURE__ */ M("div", {
870
902
  className: "row row-cols-2 row-cols-md-4 g-3",
871
903
  children: [
872
- /* @__PURE__ */ j(X, {
904
+ /* @__PURE__ */ j(Z, {
873
905
  label: "Ticket",
874
906
  children: i.number
875
907
  }),
876
- /* @__PURE__ */ j(X, {
908
+ /* @__PURE__ */ j(Z, {
877
909
  label: "Category",
878
910
  children: i.category || "-"
879
911
  }),
880
- /* @__PURE__ */ j(X, {
912
+ /* @__PURE__ */ j(Z, {
881
913
  label: "Opened",
882
914
  children: /* @__PURE__ */ j("time", {
883
915
  dateTime: i.createdDateTimeUtc,
884
- children: U(i.createdDateTimeUtc)
916
+ children: K(i.createdDateTimeUtc)
885
917
  })
886
918
  }),
887
- /* @__PURE__ */ j(X, {
919
+ /* @__PURE__ */ j(Z, {
888
920
  label: "Last update",
889
921
  children: /* @__PURE__ */ j("time", {
890
922
  dateTime: i.updatedDateTimeUtc,
891
- children: U(i.updatedDateTimeUtc)
923
+ children: K(i.updatedDateTimeUtc)
892
924
  })
893
925
  })
894
926
  ]
@@ -916,15 +948,15 @@ function Te({ ticketId: e, onBack: t, onChanged: n }) {
916
948
  className: "align-self-start",
917
949
  children: "Reopen ticket"
918
950
  }),
919
- /* @__PURE__ */ j(Ce, {
951
+ /* @__PURE__ */ j(Se, {
920
952
  satisfaction: i.satisfaction,
921
953
  comment: i.satisfactionComment,
922
954
  canRate: i.canRate,
923
955
  commentMaxLength: r?.satisfactionCommentMaxLength,
924
956
  onRate: (e) => v(() => p(e))
925
957
  }),
926
- /* @__PURE__ */ j(Se, { messages: i.messages ?? [] }),
927
- /* @__PURE__ */ j(we, {
958
+ /* @__PURE__ */ j(xe, { messages: i.messages ?? [] }),
959
+ /* @__PURE__ */ j(Ce, {
928
960
  config: r,
929
961
  reopens: x,
930
962
  onSend: (e) => v(() => u(e))
@@ -934,60 +966,50 @@ function Te({ ticketId: e, onBack: t, onChanged: n }) {
934
966
  }
935
967
  //#endregion
936
968
  //#region src/lib/url.js
937
- var Z = "ticket";
938
- function Ee() {
939
- return typeof window > "u" ? null : new URLSearchParams(window.location.search).get(Z);
969
+ var Q = "ticket";
970
+ function Te() {
971
+ return typeof window > "u" ? null : new URLSearchParams(window.location.search).get(Q);
940
972
  }
941
- function De(e) {
942
- if (typeof window > "u") return e ? `?${Z}=${encodeURIComponent(e)}` : "?";
973
+ function Ee(e) {
974
+ if (typeof window > "u") return e ? `?${Q}=${encodeURIComponent(e)}` : "?";
943
975
  let t = new URL(window.location.href);
944
- return e ? t.searchParams.set(Z, e) : t.searchParams.delete(Z), `${t.pathname}${t.search}${t.hash}`;
976
+ return e ? t.searchParams.set(Q, e) : t.searchParams.delete(Q), `${t.pathname}${t.search}${t.hash}`;
945
977
  }
946
- function Oe(e) {
978
+ function De(e) {
947
979
  if (typeof window > "u") return;
948
- let t = De(e);
980
+ let t = Ee(e);
949
981
  t !== `${window.location.pathname}${window.location.search}${window.location.hash}` && window.history.replaceState(window.history.state, "", t);
950
982
  }
951
983
  //#endregion
952
984
  //#region src/components/TicketList.jsx
953
- var Q = [
985
+ var Oe = [
954
986
  {
955
- key: "active",
956
- label: "Active"
987
+ key: "all",
988
+ label: "All",
989
+ countKey: "total"
957
990
  },
958
991
  {
959
992
  key: "Open",
960
- label: "Open"
993
+ label: "Open",
994
+ countKey: "open"
961
995
  },
962
996
  {
963
997
  key: "InProgress",
964
- label: "In progress"
998
+ label: "In progress",
999
+ countKey: "inProgress"
965
1000
  },
966
1001
  {
967
1002
  key: "Resolved",
968
- label: "Resolved"
1003
+ label: "Resolved",
1004
+ countKey: "resolved"
969
1005
  },
970
1006
  {
971
1007
  key: "Closed",
972
- label: "Closed"
973
- },
974
- {
975
- key: "all",
976
- label: "All"
1008
+ label: "Closed",
1009
+ countKey: "closed"
977
1010
  }
978
1011
  ];
979
- function ke(e, t) {
980
- return t === "all" ? !0 : t === "active" ? e.status === "Open" || e.status === "InProgress" : e.status === t;
981
- }
982
- function Ae(e, t) {
983
- return !t || [
984
- e.number,
985
- e.subject,
986
- e.category
987
- ].some((e) => (e || "").toLowerCase().includes(t));
988
- }
989
- function je({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i }) {
990
- let [o, l] = s("active"), [u, f] = s(""), p = u.trim().toLowerCase(), h = a(() => e.filter((e) => Ae(e, p)), [e, p]), g = a(() => Object.fromEntries(Q.map((e) => [e.key, h.filter((t) => ke(t, e.key)).length])), [h]), _ = k(a(() => h.filter((e) => ke(e, o)), [h, o]));
1012
+ function ke({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i, status: a, onStatusChange: o, counts: s, search: l, onSearchChange: u, pagination: f }) {
991
1013
  return /* @__PURE__ */ M("div", {
992
1014
  className: "d-flex flex-column gap-3",
993
1015
  children: [
@@ -1007,17 +1029,18 @@ function je({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i }) {
1007
1029
  /* @__PURE__ */ M("div", {
1008
1030
  className: "d-flex flex-column flex-md-row gap-2 align-items-md-center justify-content-between",
1009
1031
  children: [/* @__PURE__ */ j(D, {
1010
- tabs: Q.map((e) => ({
1011
- ...e,
1012
- count: g[e.key]
1032
+ tabs: Oe.map(({ key: e, label: t, countKey: n }) => ({
1033
+ key: e,
1034
+ label: t,
1035
+ count: s?.[n]
1013
1036
  })),
1014
- value: o,
1015
- onChange: l,
1037
+ value: a,
1038
+ onChange: o,
1016
1039
  ariaLabel: "Filter tickets by status",
1017
1040
  className: "flex-grow-1"
1018
1041
  }), /* @__PURE__ */ j(x, {
1019
- value: u,
1020
- onChange: f,
1042
+ value: l,
1043
+ onChange: u,
1021
1044
  placeholder: "Search tickets",
1022
1045
  ariaLabel: "Search tickets"
1023
1046
  })]
@@ -1031,7 +1054,7 @@ function je({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i }) {
1031
1054
  className: "d-flex flex-column",
1032
1055
  style: { minWidth: 0 },
1033
1056
  children: [/* @__PURE__ */ j(y, {
1034
- href: De(e.id),
1057
+ href: Ee(e.id),
1035
1058
  animation: "none",
1036
1059
  className: "fw-semibold text-break",
1037
1060
  onClick: (t) => {
@@ -1054,7 +1077,7 @@ function je({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i }) {
1054
1077
  label: "Status",
1055
1078
  render: (e) => e.statusLabel ? /* @__PURE__ */ j(T, {
1056
1079
  label: e.statusLabel,
1057
- tone: Y(e.status)
1080
+ tone: ge(e.status)
1058
1081
  }) : "-"
1059
1082
  },
1060
1083
  {
@@ -1069,16 +1092,16 @@ function je({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i }) {
1069
1092
  cellClassName: "wui-text-sm text-body-secondary text-nowrap",
1070
1093
  render: (e) => /* @__PURE__ */ j("time", {
1071
1094
  dateTime: e.updatedDateTimeUtc,
1072
- children: U(e.updatedDateTimeUtc)
1095
+ children: K(e.updatedDateTimeUtc)
1073
1096
  })
1074
1097
  }
1075
1098
  ],
1076
- rows: _.pageRows,
1099
+ rows: e,
1077
1100
  loading: t,
1078
1101
  skeletonRows: 4,
1079
1102
  ariaLabel: "Support tickets",
1080
- sourceCount: e.length,
1081
- pagination: _,
1103
+ sourceCount: s?.total ?? 0,
1104
+ pagination: f,
1082
1105
  empty: /* @__PURE__ */ j(E, {
1083
1106
  icon: "message-square",
1084
1107
  title: "No tickets yet",
@@ -1094,16 +1117,61 @@ function je({ tickets: e, loading: t, error: n, onRetry: r, onOpen: i }) {
1094
1117
  });
1095
1118
  }
1096
1119
  //#endregion
1120
+ //#region src/lib/useDebouncedValue.js
1121
+ function Ae(e, t) {
1122
+ let [n, i] = s(e);
1123
+ return r(() => {
1124
+ let n = setTimeout(() => i(e), t);
1125
+ return () => clearTimeout(n);
1126
+ }, [e, t]), n;
1127
+ }
1128
+ //#endregion
1097
1129
  //#region src/SupportCenter.jsx
1098
- function Me({ initialTicketId: e, defaultCategory: t, appVersion: n }) {
1099
- let { config: r, error: i, reload: a } = B(), { tickets: o, loading: l, error: u, reload: f } = V(), [p, m] = s(() => e || Ee()), [h, g] = s(!1), _ = (e) => {
1100
- m(e), Oe(e);
1130
+ var je = 300, Me = 10;
1131
+ function Ne({ initialTicketId: e, defaultCategory: t, appVersion: n }) {
1132
+ let { config: i, error: a, reload: o } = W(), [l, u] = s("all"), [f, p] = s(""), m = Ae(f.trim(), je), [h, g] = s(Me), _ = `${l}|${m}|${h}`, [v, y] = s({
1133
+ key: _,
1134
+ page: 1
1135
+ }), b = v.key === _ ? v.page : 1, x = (e) => y({
1136
+ key: _,
1137
+ page: e
1138
+ }), S = te({
1139
+ status: l === "all" ? void 0 : l,
1140
+ search: m || void 0,
1141
+ page: b,
1142
+ pageSize: h
1143
+ }), { counts: C, reload: w } = ne(), [T, E] = s(() => e || Te()), [D, O] = s(!1), k = S.totalPages;
1144
+ r(() => {
1145
+ k > 0 && b > k && y({
1146
+ key: _,
1147
+ page: k
1148
+ });
1149
+ }, [
1150
+ k,
1151
+ b,
1152
+ _
1153
+ ]);
1154
+ let A = () => {
1155
+ S.reload(), w();
1156
+ }, N = (e) => {
1157
+ E(e), De(e);
1101
1158
  };
1102
- return p ? /* @__PURE__ */ j(Te, {
1103
- ticketId: p,
1104
- onBack: () => _(null),
1105
- onChanged: f
1106
- }, p) : /* @__PURE__ */ M("section", {
1159
+ if (T) return /* @__PURE__ */ j(we, {
1160
+ ticketId: T,
1161
+ onBack: () => N(null),
1162
+ onChanged: A
1163
+ }, T);
1164
+ let P = S.totalCount === 0 ? 0 : (b - 1) * h + 1, F = {
1165
+ page: b,
1166
+ totalPages: Math.max(1, S.totalPages),
1167
+ pageSize: h,
1168
+ total: S.totalCount,
1169
+ firstRow: P,
1170
+ lastRow: Math.min(S.totalCount, P + S.tickets.length - 1),
1171
+ setPage: x,
1172
+ setPageSize: g
1173
+ };
1174
+ return /* @__PURE__ */ M("section", {
1107
1175
  className: "d-flex flex-column gap-3",
1108
1176
  "aria-labelledby": "support-center-title",
1109
1177
  children: [
@@ -1115,39 +1183,45 @@ function Me({ initialTicketId: e, defaultCategory: t, appVersion: n }) {
1115
1183
  children: "Your tickets"
1116
1184
  }), /* @__PURE__ */ j(d, {
1117
1185
  leftIcon: "plus",
1118
- onClick: () => g(!0),
1119
- disabled: !r,
1186
+ onClick: () => O(!0),
1187
+ disabled: !i,
1120
1188
  children: "New ticket"
1121
1189
  })]
1122
1190
  }),
1123
- i && /* @__PURE__ */ M(c, {
1191
+ a && /* @__PURE__ */ M(c, {
1124
1192
  variant: "warning",
1125
1193
  children: [
1126
- i.message || "We could not load the support settings.",
1194
+ a.message || "We could not load the support settings.",
1127
1195
  " ",
1128
1196
  /* @__PURE__ */ j(d, {
1129
1197
  variant: "ghost",
1130
1198
  size: "sm",
1131
- onClick: a,
1199
+ onClick: o,
1132
1200
  children: "Try again"
1133
1201
  })
1134
1202
  ]
1135
1203
  }),
1136
- /* @__PURE__ */ j(je, {
1137
- tickets: o,
1138
- loading: l,
1139
- error: u,
1140
- onRetry: f,
1141
- onOpen: _
1204
+ /* @__PURE__ */ j(ke, {
1205
+ tickets: S.tickets,
1206
+ loading: S.loading,
1207
+ error: S.error,
1208
+ onRetry: A,
1209
+ onOpen: N,
1210
+ status: l,
1211
+ onStatusChange: u,
1212
+ counts: C,
1213
+ search: f,
1214
+ onSearchChange: p,
1215
+ pagination: F
1142
1216
  }),
1143
- r && /* @__PURE__ */ j(ge, {
1144
- open: h,
1145
- onClose: () => g(!1),
1146
- config: r,
1217
+ i && /* @__PURE__ */ j(me, {
1218
+ open: D,
1219
+ onClose: () => O(!1),
1220
+ config: i,
1147
1221
  defaultCategory: t,
1148
1222
  appVersion: n,
1149
1223
  onCreated: (e) => {
1150
- g(!1), f(), _(e.id);
1224
+ O(!1), A(), N(e.id);
1151
1225
  }
1152
1226
  })
1153
1227
  ]
@@ -1156,8 +1230,8 @@ function Me({ initialTicketId: e, defaultCategory: t, appVersion: n }) {
1156
1230
  //#endregion
1157
1231
  //#region src/SupportContact.jsx
1158
1232
  var $ = (e) => typeof e == "string" && e.trim() !== "";
1159
- function Ne({ className: e }) {
1160
- let { config: t } = B();
1233
+ function Pe({ className: e }) {
1234
+ let { config: t } = W();
1161
1235
  if (!t) return null;
1162
1236
  let n = [];
1163
1237
  return $(t.supportEmail) && n.push({
@@ -1199,7 +1273,7 @@ function Ne({ className: e }) {
1199
1273
  }
1200
1274
  //#endregion
1201
1275
  //#region src/SupportProvider.jsx
1202
- function Pe({ client: e, children: n }) {
1276
+ function Fe({ client: e, children: n }) {
1203
1277
  let r = o(null), i = t((t = !1) => {
1204
1278
  let n = r.current;
1205
1279
  if (t || !n || n.client !== e) {
@@ -1216,10 +1290,10 @@ function Pe({ client: e, children: n }) {
1216
1290
  client: e,
1217
1291
  loadConfig: i
1218
1292
  }), [e, i]);
1219
- return /* @__PURE__ */ j(ie.Provider, {
1293
+ return /* @__PURE__ */ j(B.Provider, {
1220
1294
  value: s,
1221
1295
  children: n
1222
1296
  });
1223
1297
  }
1224
1298
  //#endregion
1225
- export { he as ReportIssue, P as SupportApiError, Me as SupportCenter, Ne as SupportContact, Pe as SupportProvider, re as createSupportClient, R as useSupportClient, B as useSupportConfig, H as useSupportTicket, V as useSupportTickets };
1299
+ export { pe as ReportIssue, P as SupportApiError, Ne as SupportCenter, Pe as SupportContact, Fe as SupportProvider, ee as createSupportClient, H as useSupportClient, W as useSupportConfig, G as useSupportTicket, ne as useSupportTicketCounts, te as useSupportTickets };
@@ -11,8 +11,11 @@
11
11
  * @param {(id: string) => string} [props.getTicketHref] Where the new ticket can be opened; shows a View ticket link.
12
12
  * @param {(ticket: any) => void} [props.onOpenTicket] Called by the View ticket button when there is no href.
13
13
  * @param {(ticket: any) => void} [props.onCreated] Called once the ticket is raised.
14
+ * @param {string} [props.className] Added to the button's wrapper, both floating and inline, to place it.
15
+ * @param {import('react').CSSProperties} [props.style] Merged over the wrapper's own style. With
16
+ * `floating`, an inset such as `{ bottom: "5rem" }` moves the button clear of other fixed controls.
14
17
  */
15
- export default function ReportIssue({ label, floating, defaultCategory, appVersion, getTicketHref, onOpenTicket, onCreated, }: {
18
+ export default function ReportIssue({ label, floating, defaultCategory, appVersion, getTicketHref, onOpenTicket, onCreated, className, style, }: {
16
19
  label?: string;
17
20
  floating?: boolean;
18
21
  defaultCategory?: string;
@@ -20,4 +23,6 @@ export default function ReportIssue({ label, floating, defaultCategory, appVersi
20
23
  getTicketHref?: (id: string) => string;
21
24
  onOpenTicket?: (ticket: any) => void;
22
25
  onCreated?: (ticket: any) => void;
26
+ className?: string;
27
+ style?: import('react').CSSProperties;
23
28
  }): import("react").JSX.Element;
@@ -2,6 +2,7 @@
2
2
  * The customer's support page: their tickets, one ticket's conversation, and a New ticket
3
3
  * dialog. Opens the ticket named by `?ticket=<id>` in the address (support emails link there),
4
4
  * or `initialTicketId` when given, and keeps that parameter in step as tickets open and close.
5
+ * The list is filtered, searched and paged on the server; the status tabs show the server's counts.
5
6
  *
6
7
  * @param {object} props
7
8
  * @param {string} [props.initialTicketId] Open this ticket first. Wins over the address.
@@ -44,9 +44,43 @@ export type RatingInput = {
44
44
  satisfaction: "Satisfied" | "Unsatisfied";
45
45
  comment?: string;
46
46
  };
47
+ export type TicketStatus = "Open" | "InProgress" | "Resolved" | "Closed";
48
+ export type TicketListQuery = {
49
+ /**
50
+ * Only tickets in this status; every status when left out.
51
+ */
52
+ status?: TicketStatus;
53
+ /**
54
+ * Matches the subject or the ticket number.
55
+ */
56
+ search?: string;
57
+ /**
58
+ * 1-based. The API defaults to 1.
59
+ */
60
+ page?: number;
61
+ /**
62
+ * The API defaults to 20 and allows up to 100.
63
+ */
64
+ pageSize?: number;
65
+ };
66
+ export type TicketPage = {
67
+ items: any[];
68
+ page: number;
69
+ pageSize: number;
70
+ totalCount: number;
71
+ totalPages: number;
72
+ };
73
+ export type TicketCounts = {
74
+ open: number;
75
+ inProgress: number;
76
+ resolved: number;
77
+ closed: number;
78
+ total: number;
79
+ };
47
80
  export type SupportClient = {
48
81
  getConfig: () => Promise<any>;
49
- listTickets: () => Promise<any[]>;
82
+ listTickets: (query?: TicketListQuery) => Promise<TicketPage>;
83
+ getTicketCounts: () => Promise<TicketCounts>;
50
84
  getTicket: (id: string) => Promise<any>;
51
85
  createTicket: (input: NewTicketInput) => Promise<any>;
52
86
  reply: (id: string, input: ReplyInput) => Promise<any>;
@@ -81,10 +115,37 @@ export type SupportClient = {
81
115
  * @property {"Satisfied"|"Unsatisfied"} satisfaction
82
116
  * @property {string} [comment]
83
117
  */
118
+ /**
119
+ * @typedef {"Open"|"InProgress"|"Resolved"|"Closed"} TicketStatus
120
+ */
121
+ /**
122
+ * @typedef {object} TicketListQuery
123
+ * @property {TicketStatus} [status] Only tickets in this status; every status when left out.
124
+ * @property {string} [search] Matches the subject or the ticket number.
125
+ * @property {number} [page] 1-based. The API defaults to 1.
126
+ * @property {number} [pageSize] The API defaults to 20 and allows up to 100.
127
+ */
128
+ /**
129
+ * @typedef {object} TicketPage One page of the customer's tickets, most recently updated first.
130
+ * @property {any[]} items
131
+ * @property {number} page
132
+ * @property {number} pageSize
133
+ * @property {number} totalCount
134
+ * @property {number} totalPages
135
+ */
136
+ /**
137
+ * @typedef {object} TicketCounts How many of the customer's tickets are in each status.
138
+ * @property {number} open
139
+ * @property {number} inProgress
140
+ * @property {number} resolved
141
+ * @property {number} closed
142
+ * @property {number} total
143
+ */
84
144
  /**
85
145
  * @typedef {object} SupportClient
86
146
  * @property {() => Promise<any>} getConfig
87
- * @property {() => Promise<any[]>} listTickets
147
+ * @property {(query?: TicketListQuery) => Promise<TicketPage>} listTickets
148
+ * @property {() => Promise<TicketCounts>} getTicketCounts
88
149
  * @property {(id: string) => Promise<any>} getTicket
89
150
  * @property {(input: NewTicketInput) => Promise<any>} createTicket
90
151
  * @property {(id: string, input: ReplyInput) => Promise<any>} reply
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Picked, dropped or pasted files for a message, limited to what the support config allows.
3
+ * The caller checks every file against the config and passes the one message through `error`,
4
+ * so the field's own check stays off and a rejected pick never shows two messages.
3
5
  *
4
6
  * @param {object} props
5
7
  * @param {string} [props.id]
@@ -1,17 +1,39 @@
1
1
  /**
2
- * The customer's tickets with status tabs and a search box.
2
+ * One page of the customer's tickets with status tabs and a search box. Filtering, search and
3
+ * paging all happen on the server; this only shows what it is given.
3
4
  *
4
5
  * @param {object} props
5
- * @param {any[]} props.tickets
6
+ * @param {any[]} props.tickets The current page.
6
7
  * @param {boolean} props.loading
7
8
  * @param {any} [props.error]
8
9
  * @param {() => void} props.onRetry
9
10
  * @param {(id: string) => void} props.onOpen
11
+ * @param {string} props.status A `STATUS_FILTERS` key.
12
+ * @param {(status: string) => void} props.onStatusChange
13
+ * @param {import('../client.js').TicketCounts|null} [props.counts] Labels the tabs once loaded.
14
+ * @param {string} props.search What is in the search box.
15
+ * @param {(search: string) => void} props.onSearchChange
16
+ * @param {{ page: number, totalPages: number, pageSize: number, total: number, firstRow: number, lastRow: number, setPage: (page: number) => void, setPageSize: (size: number) => void }} props.pagination
10
17
  */
11
- export default function TicketList({ tickets, loading, error, onRetry, onOpen }: {
18
+ export default function TicketList({ tickets, loading, error, onRetry, onOpen, status, onStatusChange, counts, search, onSearchChange, pagination, }: {
12
19
  tickets: any[];
13
20
  loading: boolean;
14
21
  error?: any;
15
22
  onRetry: () => void;
16
23
  onOpen: (id: string) => void;
24
+ status: string;
25
+ onStatusChange: (status: string) => void;
26
+ counts?: import('../client.js').TicketCounts | null;
27
+ search: string;
28
+ onSearchChange: (search: string) => void;
29
+ pagination: {
30
+ page: number;
31
+ totalPages: number;
32
+ pageSize: number;
33
+ total: number;
34
+ firstRow: number;
35
+ lastRow: number;
36
+ setPage: (page: number) => void;
37
+ setPageSize: (size: number) => void;
38
+ };
17
39
  }): import("react").JSX.Element;
@@ -21,12 +21,27 @@ export declare function useSupportConfig(): {
21
21
  reload: () => void;
22
22
  };
23
23
  /**
24
- * The signed-in customer's tickets.
24
+ * One page of the signed-in customer's tickets, filtered and searched on the server. Loads again
25
+ * whenever any part of the query changes; an answer for a query that has since moved on is dropped.
25
26
  *
26
- * @returns {{ tickets: any[], loading: boolean, error: any, reload: () => void }}
27
+ * @param {import('./client.js').TicketListQuery} [query]
28
+ * @returns {{ tickets: any[], totalCount: number, totalPages: number, loading: boolean, error: any, reload: () => void }}
27
29
  */
28
- export declare function useSupportTickets(): {
30
+ export declare function useSupportTickets(query?: import('./client.js').TicketListQuery): {
29
31
  tickets: any[];
32
+ totalCount: number;
33
+ totalPages: number;
34
+ loading: boolean;
35
+ error: any;
36
+ reload: () => void;
37
+ };
38
+ /**
39
+ * How many of the signed-in customer's tickets are in each status, for labelling status tabs.
40
+ *
41
+ * @returns {{ counts: import('./client.js').TicketCounts|null, loading: boolean, error: any, reload: () => void }}
42
+ */
43
+ export declare function useSupportTicketCounts(): {
44
+ counts: import('./client.js').TicketCounts | null;
30
45
  loading: boolean;
31
46
  error: any;
32
47
  reload: () => void;
@@ -1,5 +1,5 @@
1
1
  export { createSupportClient, SupportApiError } from "./client.js";
2
- export { useSupportClient, useSupportConfig, useSupportTicket, useSupportTickets } from "./context.js";
2
+ export { useSupportClient, useSupportConfig, useSupportTicket, useSupportTicketCounts, useSupportTickets } from "./context.js";
3
3
  export { default as ReportIssue } from "./ReportIssue.jsx";
4
4
  export { default as SupportCenter } from "./SupportCenter.jsx";
5
5
  export { default as SupportContact } from "./SupportContact.jsx";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `value`, once it has stopped changing for `delayMs`. Lets a search box ask the server once
3
+ * per pause in typing instead of once per key.
4
+ *
5
+ * @template T
6
+ * @param {T} value
7
+ * @param {number} delayMs
8
+ * @returns {T}
9
+ */
10
+ export declare function useDebouncedValue<T>(value: T, delayMs: number): T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/support-react",
3
- "version": "0.13.0",
3
+ "version": "0.13.3",
4
4
  "description": "Webority customer support for React portals: ticket centre, report-an-issue with a page screenshot, and support contact details, built on @webority/ui-react.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "react": "^19.0.0",
33
33
  "react-bootstrap": "^2.10.0",
34
34
  "react-dom": "^19.0.0",
35
- "@webority/ui-react": "0.13.0"
35
+ "@webority/ui-react": "0.13.3"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/dom": "10.4.2",
@@ -50,7 +50,7 @@
50
50
  "typescript": "^7.0.2",
51
51
  "vite": "^8.0.12",
52
52
  "vitest": "5.0.1",
53
- "@webority/ui-react": "0.13.0"
53
+ "@webority/ui-react": "0.13.3"
54
54
  },
55
55
  "dependencies": {
56
56
  "html-to-image": "^1.11.13"