@flamingo-stack/openframe-frontend-core 0.0.216 → 0.0.217
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/{chunk-SMCG2CCC.cjs → chunk-6DCKL73F.cjs} +24 -24
- package/dist/{chunk-SMCG2CCC.cjs.map → chunk-6DCKL73F.cjs.map} +1 -1
- package/dist/{chunk-QTKU6ULP.js → chunk-BVFRD34B.js} +2 -2
- package/dist/{chunk-CDLYRFDE.js → chunk-ENBGG2K2.js} +3767 -3610
- package/dist/chunk-ENBGG2K2.js.map +1 -0
- package/dist/{chunk-K4DFAVSO.cjs → chunk-G2HHSZ3S.cjs} +9 -9
- package/dist/{chunk-K4DFAVSO.cjs.map → chunk-G2HHSZ3S.cjs.map} +1 -1
- package/dist/{chunk-2V4SACHE.js → chunk-L6IBKPVM.js} +2 -2
- package/dist/{chunk-572WQWIX.cjs → chunk-MVQ3OODK.cjs} +9 -9
- package/dist/{chunk-572WQWIX.cjs.map → chunk-MVQ3OODK.cjs.map} +1 -1
- package/dist/{chunk-GVNQAGXB.js → chunk-N5IKPYRL.js} +3 -81
- package/dist/chunk-N5IKPYRL.js.map +1 -0
- package/dist/{chunk-VC3ND5RB.js → chunk-SWZUZYWR.js} +2 -2
- package/dist/{chunk-IH76P5R6.cjs → chunk-TYIBMDUZ.cjs} +8 -86
- package/dist/chunk-TYIBMDUZ.cjs.map +1 -0
- package/dist/{chunk-ZGTDUPTW.cjs → chunk-YWDC5BXM.cjs} +382 -225
- package/dist/chunk-YWDC5BXM.cjs.map +1 -0
- package/dist/components/chat/chat-attachment-bar.d.ts +13 -2
- package/dist/components/chat/chat-attachment-bar.d.ts.map +1 -1
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-message-row.d.ts +25 -0
- package/dist/components/chat/chat-message-row.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +6 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +5 -1
- package/dist/components/chat/types/component.types.d.ts +8 -1
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/form.d.ts +1 -1
- package/dist/components/form.d.ts.map +1 -1
- package/dist/components/index.cjs +56 -52
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +9 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +18 -18
- package/dist/components/onboarding-guides/index.js +3 -3
- package/dist/components/shared/dev-section/dev-card-row.d.ts +5 -45
- package/dist/components/shared/dev-section/dev-card-row.d.ts.map +1 -1
- package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-ticket-engagements.d.ts +9 -1
- package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-tickets-list.d.ts +7 -0
- package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +294 -255
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.d.ts +1 -0
- package/dist/components/tickets/index.d.ts.map +1 -1
- package/dist/components/tickets/index.js +360 -321
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
- package/dist/components/tickets/ticket-reply-composer.d.ts +33 -0
- package/dist/components/tickets/ticket-reply-composer.d.ts.map +1 -0
- package/dist/components/tickets/types.d.ts +13 -0
- package/dist/components/tickets/types.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -1
- package/dist/components/ui/ticket-attachments-list.d.ts +5 -1
- package/dist/components/ui/ticket-attachments-list.d.ts.map +1 -1
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -1
- package/dist/utils/index.cjs +59 -4
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +59 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/scroll-into-view.d.ts +43 -48
- package/dist/utils/scroll-into-view.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-attachment-bar.tsx +58 -22
- package/src/components/chat/chat-input.tsx +68 -29
- package/src/components/chat/chat-message-row.tsx +124 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/types/component.types.ts +8 -1
- package/src/components/shared/dev-section/dev-card-row.tsx +5 -183
- package/src/components/shared/legal-document/use-legal-docs.ts +5 -1
- package/src/components/tickets/help-center-card.tsx +26 -29
- package/src/components/tickets/help-center-list.tsx +57 -10
- package/src/components/tickets/hooks/use-ticket-engagements.ts +17 -1
- package/src/components/tickets/hooks/use-tickets-list.ts +13 -0
- package/src/components/tickets/index.ts +4 -0
- package/src/components/tickets/ticket-detail-drawer.tsx +144 -200
- package/src/components/tickets/ticket-reply-composer.tsx +195 -0
- package/src/components/tickets/types.ts +14 -0
- package/src/components/ui/ticket-attachments-list.tsx +26 -8
- package/src/styles/app-globals.css +13 -0
- package/src/utils/scroll-into-view.ts +127 -53
- package/dist/chunk-CDLYRFDE.js.map +0 -1
- package/dist/chunk-GVNQAGXB.js.map +0 -1
- package/dist/chunk-IH76P5R6.cjs.map +0 -1
- package/dist/chunk-ZGTDUPTW.cjs.map +0 -1
- /package/dist/{chunk-QTKU6ULP.js.map → chunk-BVFRD34B.js.map} +0 -0
- /package/dist/{chunk-2V4SACHE.js.map → chunk-L6IBKPVM.js.map} +0 -0
- /package/dist/{chunk-VC3ND5RB.js.map → chunk-SWZUZYWR.js.map} +0 -0
|
@@ -4,18 +4,16 @@ import {
|
|
|
4
4
|
EmptyState,
|
|
5
5
|
UnifiedPagination,
|
|
6
6
|
init_unified_pagination
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-L6IBKPVM.js";
|
|
8
8
|
import {
|
|
9
|
-
ConversationCardRow,
|
|
10
|
-
ConversationCardRowSkeletonList,
|
|
11
9
|
DevCardRowContent,
|
|
12
10
|
DevCardRowSkeletonList,
|
|
13
11
|
DevSectionPage
|
|
14
|
-
} from "../../chunk-
|
|
12
|
+
} from "../../chunk-N5IKPYRL.js";
|
|
15
13
|
import "../../chunk-ORJREQ2W.js";
|
|
16
14
|
import {
|
|
17
15
|
ContactForm
|
|
18
|
-
} from "../../chunk-
|
|
16
|
+
} from "../../chunk-BVFRD34B.js";
|
|
19
17
|
import {
|
|
20
18
|
AlertDialog,
|
|
21
19
|
AlertDialogAction,
|
|
@@ -28,18 +26,22 @@ import {
|
|
|
28
26
|
Card,
|
|
29
27
|
ChatAttachmentAddButton,
|
|
30
28
|
ChatAttachmentChipStrip,
|
|
29
|
+
ChatInput,
|
|
30
|
+
ChatMessageRow,
|
|
31
|
+
ChatMessageRowSkeleton,
|
|
31
32
|
ChatTicketItem,
|
|
32
33
|
Label,
|
|
33
34
|
SquareAvatar,
|
|
34
35
|
StatusBadge,
|
|
35
36
|
Textarea,
|
|
37
|
+
TicketAttachmentsList,
|
|
36
38
|
embedAuthedFetch,
|
|
37
39
|
formatRelativeTime,
|
|
38
40
|
getStatusColorScheme,
|
|
39
41
|
scrollElementIntoView,
|
|
40
42
|
useChatAttachments,
|
|
41
43
|
useChatIdentity
|
|
42
|
-
} from "../../chunk-
|
|
44
|
+
} from "../../chunk-ENBGG2K2.js";
|
|
43
45
|
import "../../chunk-LXC6P2EO.js";
|
|
44
46
|
import "../../chunk-EL5YVPD5.js";
|
|
45
47
|
import {
|
|
@@ -68,7 +70,7 @@ import "../../chunk-CZR7ARBA.js";
|
|
|
68
70
|
import "../../chunk-GGWZFCYS.js";
|
|
69
71
|
|
|
70
72
|
// src/components/tickets/ticket-center.tsx
|
|
71
|
-
import { useCallback as
|
|
73
|
+
import { useCallback as useCallback4, useState as useState4 } from "react";
|
|
72
74
|
import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
|
|
73
75
|
init_button();
|
|
74
76
|
import { RefreshCw } from "lucide-react";
|
|
@@ -82,6 +84,7 @@ function isOptimistic(t) {
|
|
|
82
84
|
return t._optimistic === true;
|
|
83
85
|
}
|
|
84
86
|
var TICKET_TEXT_MAX_CHARS = 5e3;
|
|
87
|
+
var TICKET_LIVE_POLL_MS = 8e3;
|
|
85
88
|
var TOAST_COPY = {
|
|
86
89
|
open_success: { title: "Ticket opened", description: "We received your message and will follow up shortly." },
|
|
87
90
|
open_mirror_pending: { title: "Ticket opened", description: "Syncing \u2014 your ticket will appear momentarily." },
|
|
@@ -217,7 +220,7 @@ function TicketOpenForm({
|
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
// src/components/tickets/ticket-row.tsx
|
|
220
|
-
import { useCallback, useRef } from "react";
|
|
223
|
+
import { useCallback as useCallback2, useRef } from "react";
|
|
221
224
|
|
|
222
225
|
// src/components/collapsible.tsx
|
|
223
226
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
@@ -226,12 +229,12 @@ var CollapsibleContent2 = CollapsiblePrimitive.CollapsibleContent;
|
|
|
226
229
|
|
|
227
230
|
// src/components/tickets/ticket-detail-drawer.tsx
|
|
228
231
|
init_button();
|
|
229
|
-
import {
|
|
232
|
+
import { useStickToBottom } from "use-stick-to-bottom";
|
|
230
233
|
|
|
231
234
|
// src/components/tickets/hooks/use-ticket-engagements.ts
|
|
232
235
|
import { useQuery } from "@tanstack/react-query";
|
|
233
236
|
var LIST_ENGAGEMENTS_ENDPOINT = "/api/chat/agent/list-engagements";
|
|
234
|
-
function useTicketEngagements(externalTicketId, enabled = true) {
|
|
237
|
+
function useTicketEngagements(externalTicketId, enabled = true, refetchInterval = false) {
|
|
235
238
|
const identity = useChatIdentity();
|
|
236
239
|
const identityKey = identity.user?.email ?? "anon";
|
|
237
240
|
const queryEnabled = enabled && identity.authTier !== "anon" && !!identity.user?.email && !!externalTicketId && !externalTicketId.startsWith("temp-");
|
|
@@ -245,6 +248,11 @@ function useTicketEngagements(externalTicketId, enabled = true) {
|
|
|
245
248
|
gcTime: 0,
|
|
246
249
|
refetchOnMount: "always",
|
|
247
250
|
refetchOnWindowFocus: true,
|
|
251
|
+
// Live conversation: poll while the caller opts in (drawer open). New
|
|
252
|
+
// agent replies + attachments appear within one interval without a
|
|
253
|
+
// manual refresh. `refetchIntervalInBackground` stays false (default)
|
|
254
|
+
// so polling pauses on a hidden tab.
|
|
255
|
+
refetchInterval,
|
|
248
256
|
queryFn: async () => {
|
|
249
257
|
const response = await embedAuthedFetch(LIST_ENGAGEMENTS_ENDPOINT, {
|
|
250
258
|
method: "POST",
|
|
@@ -305,8 +313,134 @@ function TicketLinkedDeliveryCard({
|
|
|
305
313
|
);
|
|
306
314
|
}
|
|
307
315
|
|
|
308
|
-
// src/components/tickets/ticket-
|
|
316
|
+
// src/components/tickets/ticket-reply-composer.tsx
|
|
317
|
+
init_button();
|
|
318
|
+
import { useCallback, useState as useState2 } from "react";
|
|
309
319
|
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
320
|
+
function TicketReplyComposer({
|
|
321
|
+
ticket,
|
|
322
|
+
busy,
|
|
323
|
+
supportSystemDown,
|
|
324
|
+
onSendMessage,
|
|
325
|
+
onClose
|
|
326
|
+
}) {
|
|
327
|
+
const [resolution, setResolution] = useState2("");
|
|
328
|
+
const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
|
|
329
|
+
const attachments = useChatAttachments();
|
|
330
|
+
const ticketRef = { id: ticket.id, external_id: ticket.external_id };
|
|
331
|
+
const hasReadyFiles = attachments.readyAttachments.length > 0;
|
|
332
|
+
const handleSend = useCallback(
|
|
333
|
+
async (text) => {
|
|
334
|
+
const ref = { id: ticket.id, external_id: ticket.external_id };
|
|
335
|
+
const ok = await onSendMessage(ref, text.trim(), attachments.readyAttachments);
|
|
336
|
+
if (ok) attachments.clear();
|
|
337
|
+
return ok;
|
|
338
|
+
},
|
|
339
|
+
// Depend on the reactive projections, not the whole bag (a fresh object each
|
|
340
|
+
// render). `readyAttachments` is memo-stable; `clear` is callback-stable.
|
|
341
|
+
[
|
|
342
|
+
onSendMessage,
|
|
343
|
+
ticket.id,
|
|
344
|
+
ticket.external_id,
|
|
345
|
+
attachments.readyAttachments,
|
|
346
|
+
attachments.clear
|
|
347
|
+
]
|
|
348
|
+
);
|
|
349
|
+
const confirmClose = async () => {
|
|
350
|
+
setCloseDialogOpen(false);
|
|
351
|
+
await onClose(ticketRef, resolution.trim() || void 0);
|
|
352
|
+
setResolution("");
|
|
353
|
+
};
|
|
354
|
+
const disabled = busy || supportSystemDown;
|
|
355
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
|
|
356
|
+
/* @__PURE__ */ jsx3(
|
|
357
|
+
ChatAttachmentChipStrip,
|
|
358
|
+
{
|
|
359
|
+
attachments: attachments.attachments,
|
|
360
|
+
onRemove: attachments.removeAttachment,
|
|
361
|
+
disabled,
|
|
362
|
+
size: "compact"
|
|
363
|
+
}
|
|
364
|
+
),
|
|
365
|
+
/* @__PURE__ */ jsx3(
|
|
366
|
+
ChatInput,
|
|
367
|
+
{
|
|
368
|
+
fullWidth: true,
|
|
369
|
+
autoFocus: true,
|
|
370
|
+
placeholder: "Type a reply\u2026",
|
|
371
|
+
sending: busy || attachments.hasInflightUploads,
|
|
372
|
+
disabled: supportSystemDown,
|
|
373
|
+
allowEmptySend: hasReadyFiles,
|
|
374
|
+
maxLength: TICKET_TEXT_MAX_CHARS,
|
|
375
|
+
onSend: handleSend
|
|
376
|
+
}
|
|
377
|
+
),
|
|
378
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", children: [
|
|
379
|
+
!supportSystemDown && /* @__PURE__ */ jsx3(
|
|
380
|
+
ChatAttachmentAddButton,
|
|
381
|
+
{
|
|
382
|
+
attachmentsEnabled: true,
|
|
383
|
+
attachmentsCount: attachments.attachments.length,
|
|
384
|
+
onAddFiles: attachments.addFiles,
|
|
385
|
+
disabled,
|
|
386
|
+
size: "compact"
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
/* @__PURE__ */ jsx3("div", { className: "flex-1 min-w-0" }),
|
|
390
|
+
/* @__PURE__ */ jsx3(
|
|
391
|
+
Button,
|
|
392
|
+
{
|
|
393
|
+
type: "button",
|
|
394
|
+
variant: "transparent",
|
|
395
|
+
size: "small",
|
|
396
|
+
onClick: () => setCloseDialogOpen(true),
|
|
397
|
+
disabled,
|
|
398
|
+
className: "text-ods-text-secondary hover:text-ods-text-primary",
|
|
399
|
+
children: "Close ticket"
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
] }),
|
|
403
|
+
/* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
|
|
404
|
+
/* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
|
|
405
|
+
/* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary", children: "Close this ticket?" }),
|
|
406
|
+
/* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
|
|
407
|
+
] }),
|
|
408
|
+
/* @__PURE__ */ jsx3(
|
|
409
|
+
Textarea,
|
|
410
|
+
{
|
|
411
|
+
value: resolution,
|
|
412
|
+
onChange: (e) => setResolution(e.target.value),
|
|
413
|
+
placeholder: "Resolution (optional)",
|
|
414
|
+
rows: 3,
|
|
415
|
+
maxLength: TICKET_TEXT_MAX_CHARS,
|
|
416
|
+
className: "mt-2"
|
|
417
|
+
}
|
|
418
|
+
),
|
|
419
|
+
/* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
|
|
420
|
+
/* @__PURE__ */ jsx3(
|
|
421
|
+
AlertDialogCancel,
|
|
422
|
+
{
|
|
423
|
+
disabled: busy,
|
|
424
|
+
className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
|
|
425
|
+
children: "Cancel"
|
|
426
|
+
}
|
|
427
|
+
),
|
|
428
|
+
/* @__PURE__ */ jsx3(
|
|
429
|
+
AlertDialogAction,
|
|
430
|
+
{
|
|
431
|
+
onClick: () => void confirmClose(),
|
|
432
|
+
disabled: busy,
|
|
433
|
+
className: "bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover",
|
|
434
|
+
children: "Close ticket"
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
] })
|
|
438
|
+
] }) })
|
|
439
|
+
] });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/components/tickets/ticket-detail-drawer.tsx
|
|
443
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
310
444
|
function TicketDetailDrawer({
|
|
311
445
|
ticket,
|
|
312
446
|
busy,
|
|
@@ -319,22 +453,22 @@ function TicketDetailDrawer({
|
|
|
319
453
|
onClearReplyError
|
|
320
454
|
}) {
|
|
321
455
|
const isClosed = (ticket.status ?? "").toUpperCase() === "CLOSED";
|
|
322
|
-
return /* @__PURE__ */
|
|
323
|
-
/* @__PURE__ */
|
|
324
|
-
ticket.clickup && /* @__PURE__ */
|
|
325
|
-
/* @__PURE__ */
|
|
326
|
-
/* @__PURE__ */
|
|
327
|
-
/* @__PURE__ */
|
|
456
|
+
return /* @__PURE__ */ jsxs3("div", { className: "bg-ods-card border-t border-ods-border px-4 py-4 flex flex-col gap-4", children: [
|
|
457
|
+
/* @__PURE__ */ jsx4(AssignedAgentRow, { assignedOwner: ticket.assignedOwner }),
|
|
458
|
+
ticket.clickup && /* @__PURE__ */ jsx4(TicketLinkedDeliveryCard, { clickup: ticket.clickup }),
|
|
459
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
460
|
+
/* @__PURE__ */ jsx4("p", { className: "text-xs font-medium text-ods-text-secondary mb-2 uppercase tracking-wider", children: "Conversation" }),
|
|
461
|
+
/* @__PURE__ */ jsx4(TicketTimelinePanel, { ticket })
|
|
328
462
|
] }),
|
|
329
|
-
/* @__PURE__ */
|
|
330
|
-
replyError && /* @__PURE__ */
|
|
463
|
+
/* @__PURE__ */ jsxs3("div", { className: "border-t border-ods-border pt-4", children: [
|
|
464
|
+
replyError && /* @__PURE__ */ jsx4(
|
|
331
465
|
ReplyFailureBanner,
|
|
332
466
|
{
|
|
333
467
|
error: replyError,
|
|
334
468
|
onDismiss: onClearReplyError ?? (() => void 0)
|
|
335
469
|
}
|
|
336
470
|
),
|
|
337
|
-
isClosed ? /* @__PURE__ */
|
|
471
|
+
isClosed ? /* @__PURE__ */ jsx4(
|
|
338
472
|
ReopenAction,
|
|
339
473
|
{
|
|
340
474
|
ticketRef: { id: ticket.id, external_id: ticket.external_id },
|
|
@@ -343,37 +477,49 @@ function TicketDetailDrawer({
|
|
|
343
477
|
onReopen,
|
|
344
478
|
onActionCollapsed
|
|
345
479
|
}
|
|
346
|
-
) : /* @__PURE__ */
|
|
347
|
-
|
|
480
|
+
) : /* @__PURE__ */ jsx4(
|
|
481
|
+
TicketReplyComposer,
|
|
348
482
|
{
|
|
349
483
|
ticket,
|
|
350
484
|
busy,
|
|
351
485
|
supportSystemDown,
|
|
352
486
|
onSendMessage,
|
|
353
|
-
onClose
|
|
354
|
-
onActionCollapsed
|
|
487
|
+
onClose
|
|
355
488
|
}
|
|
356
489
|
)
|
|
357
490
|
] })
|
|
358
491
|
] });
|
|
359
492
|
}
|
|
360
493
|
var TURN_SEPARATOR_RE = /[\s]{1,16}---[\s]{1,16}/g;
|
|
494
|
+
var TICKET_FEED_FRAME = "bg-ods-card border border-ods-border rounded-[6px] overflow-y-auto w-full";
|
|
495
|
+
var TICKET_FEED_HEIGHT = "h-[60vh] md:h-[420px]";
|
|
496
|
+
var TICKET_FEED_INNER = "flex flex-col gap-4 md:gap-6 px-4 md:px-6 py-4 md:py-6";
|
|
497
|
+
var TICKET_FEED_SKELETON_ROWS = 6;
|
|
361
498
|
function TicketTimelinePanel({ ticket }) {
|
|
362
499
|
const identity = useChatIdentity();
|
|
363
500
|
const externalId = isOptimistic(ticket) ? null : ticket.external_id;
|
|
364
|
-
const { engagements, isLoading } = useTicketEngagements(
|
|
501
|
+
const { engagements, isLoading } = useTicketEngagements(
|
|
502
|
+
externalId,
|
|
503
|
+
!!externalId,
|
|
504
|
+
TICKET_LIVE_POLL_MS
|
|
505
|
+
);
|
|
506
|
+
const { scrollRef, contentRef } = useStickToBottom({ initial: "instant", resize: "smooth" });
|
|
365
507
|
const bodyTurns = ticket.body ? ticket.body.split(TURN_SEPARATOR_RE).map((t) => t.trim()).filter(Boolean) : [];
|
|
508
|
+
const customerEngagementBodies = new Set(
|
|
509
|
+
engagements.filter((e) => e.authorRole === "customer").map((e) => (e.body ?? "").trim()).filter(Boolean)
|
|
510
|
+
);
|
|
511
|
+
const suppressBodyTurnZero = bodyTurns.length > 0 && customerEngagementBodies.has(bodyTurns[0]);
|
|
366
512
|
const sessionEmailLower = identity.user?.email?.trim().toLowerCase() ?? null;
|
|
367
513
|
const isViewerTheCustomer = !!sessionEmailLower && ticket.customer_emails.some((e) => e.trim().toLowerCase() === sessionEmailLower);
|
|
368
514
|
const viewerName = identity.user?.name?.trim() || null;
|
|
369
515
|
const ticketCustomerName = ticket.customer_name?.trim() || null;
|
|
370
516
|
const customerName = (isViewerTheCustomer ? viewerName : null) || ticketCustomerName || viewerName || identity.user?.email || "You";
|
|
371
517
|
const customerAvatar = isViewerTheCustomer ? identity.user?.avatarUrl ?? void 0 : void 0;
|
|
518
|
+
if (isLoading) {
|
|
519
|
+
return /* @__PURE__ */ jsx4("div", { className: `${TICKET_FEED_FRAME} ${TICKET_FEED_HEIGHT}`, children: /* @__PURE__ */ jsx4("div", { className: TICKET_FEED_INNER, children: Array.from({ length: TICKET_FEED_SKELETON_ROWS }, (_, i) => /* @__PURE__ */ jsx4(ChatMessageRowSkeleton, {}, i)) }) });
|
|
520
|
+
}
|
|
372
521
|
if (bodyTurns.length === 0 && engagements.length === 0) {
|
|
373
|
-
|
|
374
|
-
return /* @__PURE__ */ jsx3(ConversationCardRowSkeletonList, { rows: 2 });
|
|
375
|
-
}
|
|
376
|
-
return /* @__PURE__ */ jsx3(
|
|
522
|
+
return /* @__PURE__ */ jsx4(
|
|
377
523
|
EmptyState,
|
|
378
524
|
{
|
|
379
525
|
type: "generic",
|
|
@@ -383,19 +529,17 @@ function TicketTimelinePanel({ ticket }) {
|
|
|
383
529
|
}
|
|
384
530
|
);
|
|
385
531
|
}
|
|
386
|
-
return /* @__PURE__ */
|
|
532
|
+
return /* @__PURE__ */ jsx4("div", { ref: scrollRef, className: `${TICKET_FEED_FRAME} ${TICKET_FEED_HEIGHT}`, children: /* @__PURE__ */ jsxs3("div", { ref: contentRef, className: TICKET_FEED_INNER, children: [
|
|
387
533
|
bodyTurns.map((turn, i) => {
|
|
534
|
+
if (i === 0 && suppressBodyTurnZero) return null;
|
|
388
535
|
const isResolution = turn.startsWith("[Resolution]");
|
|
389
|
-
const role = i === 0 ? "Original message" : isResolution ? "Resolution" : `Update ${i}`;
|
|
390
536
|
const text = isResolution ? turn.replace(/^\[Resolution\]\s*/, "") : turn;
|
|
391
|
-
return /* @__PURE__ */
|
|
392
|
-
|
|
537
|
+
return /* @__PURE__ */ jsx4(
|
|
538
|
+
ChatMessageRow,
|
|
393
539
|
{
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
body: text,
|
|
398
|
-
variant: "current-user"
|
|
540
|
+
displayName: customerName,
|
|
541
|
+
avatarUrl: customerAvatar,
|
|
542
|
+
body: text
|
|
399
543
|
},
|
|
400
544
|
`body-${i}-${turn.slice(0, 24)}`
|
|
401
545
|
);
|
|
@@ -418,32 +562,30 @@ function TicketTimelinePanel({ ticket }) {
|
|
|
418
562
|
author = "Support team";
|
|
419
563
|
avatarSrc = void 0;
|
|
420
564
|
}
|
|
421
|
-
|
|
422
|
-
|
|
565
|
+
const engAttachments = mapEngagementAttachments(eng.attachments);
|
|
566
|
+
return /* @__PURE__ */ jsx4(
|
|
567
|
+
ChatMessageRow,
|
|
423
568
|
{
|
|
424
|
-
author,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
timestamp: eng.createdAt,
|
|
569
|
+
displayName: author,
|
|
570
|
+
avatarUrl: avatarSrc,
|
|
571
|
+
timeLabel: eng.createdAt ? formatRelativeTime(eng.createdAt) : null,
|
|
428
572
|
body: stripAttachmentsPreamble(eng.body ?? ""),
|
|
429
|
-
|
|
430
|
-
variant: isCustomer ? "current-user" : "support"
|
|
573
|
+
footer: engAttachments.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-2", children: /* @__PURE__ */ jsx4(TicketAttachmentsList, { attachments: engAttachments, size: "compact" }) }) : null
|
|
431
574
|
},
|
|
432
575
|
eng.id
|
|
433
576
|
);
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
// content rendered — drawer is showing the customer's body
|
|
437
|
-
// turns + cached engagements while a background refetch is
|
|
438
|
-
// in flight. Single row keeps the placeholder modest.
|
|
439
|
-
/* @__PURE__ */ jsx3(ConversationCardRowSkeletonList, { rows: 1 })
|
|
440
|
-
] });
|
|
577
|
+
})
|
|
578
|
+
] }) });
|
|
441
579
|
}
|
|
442
580
|
function mapEngagementAttachments(files) {
|
|
443
581
|
return files.map((f) => ({
|
|
444
582
|
id: f.id,
|
|
445
583
|
fileName: f.name ?? `file-${f.id}`,
|
|
446
584
|
fileSize: f.size ? formatBytes(f.size) : "",
|
|
585
|
+
// Show an inline thumbnail for image attachments (the signed `url` is a
|
|
586
|
+
// viewable URL). Non-images fall back to the file-type icon. SquareAvatar
|
|
587
|
+
// degrades to initials on a broken/expired image URL.
|
|
588
|
+
thumbnailSrc: f.url && (f.mime?.startsWith("image/") ?? false) ? f.url : void 0,
|
|
447
589
|
onDownload: f.url ? () => window.open(f.url, "_blank", "noopener,noreferrer") : void 0
|
|
448
590
|
}));
|
|
449
591
|
}
|
|
@@ -466,10 +608,12 @@ function ReopenAction({
|
|
|
466
608
|
const handleReopen = async () => {
|
|
467
609
|
void await onReopen(ticketRef);
|
|
468
610
|
};
|
|
469
|
-
return /* @__PURE__ */
|
|
611
|
+
return /* @__PURE__ */ jsx4("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx4(
|
|
470
612
|
Button,
|
|
471
613
|
{
|
|
472
614
|
type: "button",
|
|
615
|
+
variant: "outline",
|
|
616
|
+
size: "small",
|
|
473
617
|
onClick: () => void handleReopen(),
|
|
474
618
|
disabled: busy || supportSystemDown,
|
|
475
619
|
loading: busy,
|
|
@@ -477,146 +621,19 @@ function ReopenAction({
|
|
|
477
621
|
}
|
|
478
622
|
) });
|
|
479
623
|
}
|
|
480
|
-
function OpenActions({
|
|
481
|
-
ticket,
|
|
482
|
-
busy,
|
|
483
|
-
supportSystemDown,
|
|
484
|
-
onSendMessage,
|
|
485
|
-
onClose,
|
|
486
|
-
onActionCollapsed
|
|
487
|
-
}) {
|
|
488
|
-
const [messageText, setMessageText] = useState2("");
|
|
489
|
-
const [resolution, setResolution] = useState2("");
|
|
490
|
-
const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
|
|
491
|
-
const attachments = useChatAttachments();
|
|
492
|
-
const disabled = busy || supportSystemDown;
|
|
493
|
-
const ticketRef = { id: ticket.id, external_id: ticket.external_id };
|
|
494
|
-
const hasText = messageText.trim().length > 0;
|
|
495
|
-
const hasReadyFiles = attachments.readyAttachments.length > 0;
|
|
496
|
-
const canSend = !disabled && (hasText || hasReadyFiles) && !attachments.hasInflightUploads;
|
|
497
|
-
const sendMessage = async () => {
|
|
498
|
-
if (!canSend) return;
|
|
499
|
-
const ok = await onSendMessage(ticketRef, messageText.trim(), attachments.readyAttachments);
|
|
500
|
-
if (ok) {
|
|
501
|
-
setMessageText("");
|
|
502
|
-
attachments.clear();
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
const confirmClose = async () => {
|
|
506
|
-
setCloseDialogOpen(false);
|
|
507
|
-
await onClose(ticketRef, resolution.trim() || void 0);
|
|
508
|
-
setResolution("");
|
|
509
|
-
};
|
|
510
|
-
return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-3", children: [
|
|
511
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
|
|
512
|
-
/* @__PURE__ */ jsx3(
|
|
513
|
-
Textarea,
|
|
514
|
-
{
|
|
515
|
-
value: messageText,
|
|
516
|
-
onChange: (e) => setMessageText(e.target.value),
|
|
517
|
-
placeholder: "Type a reply\u2026 (attach files if needed)",
|
|
518
|
-
disabled,
|
|
519
|
-
rows: 3,
|
|
520
|
-
maxLength: TICKET_TEXT_MAX_CHARS
|
|
521
|
-
}
|
|
522
|
-
),
|
|
523
|
-
/* @__PURE__ */ jsx3(
|
|
524
|
-
ChatAttachmentChipStrip,
|
|
525
|
-
{
|
|
526
|
-
attachments: attachments.attachments,
|
|
527
|
-
onRemove: attachments.removeAttachment,
|
|
528
|
-
disabled
|
|
529
|
-
}
|
|
530
|
-
),
|
|
531
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex justify-between items-center gap-2 flex-wrap", children: [
|
|
532
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
533
|
-
/* @__PURE__ */ jsx3(
|
|
534
|
-
ChatAttachmentAddButton,
|
|
535
|
-
{
|
|
536
|
-
attachmentsEnabled: !supportSystemDown,
|
|
537
|
-
attachmentsCount: attachments.attachments.length,
|
|
538
|
-
onAddFiles: attachments.addFiles,
|
|
539
|
-
disabled
|
|
540
|
-
}
|
|
541
|
-
),
|
|
542
|
-
/* @__PURE__ */ jsx3("span", { className: "text-xs text-ods-text-secondary", children: "Attach files" })
|
|
543
|
-
] }),
|
|
544
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
|
|
545
|
-
/* @__PURE__ */ jsx3(
|
|
546
|
-
Button,
|
|
547
|
-
{
|
|
548
|
-
type: "button",
|
|
549
|
-
variant: "transparent",
|
|
550
|
-
onClick: () => setCloseDialogOpen(true),
|
|
551
|
-
disabled,
|
|
552
|
-
className: "bg-ods-error hover:bg-ods-error-hover text-white border-transparent",
|
|
553
|
-
children: "Close ticket"
|
|
554
|
-
}
|
|
555
|
-
),
|
|
556
|
-
/* @__PURE__ */ jsx3(
|
|
557
|
-
Button,
|
|
558
|
-
{
|
|
559
|
-
type: "button",
|
|
560
|
-
onClick: () => void sendMessage(),
|
|
561
|
-
disabled: !canSend,
|
|
562
|
-
loading: busy,
|
|
563
|
-
children: "Send reply"
|
|
564
|
-
}
|
|
565
|
-
)
|
|
566
|
-
] })
|
|
567
|
-
] })
|
|
568
|
-
] }),
|
|
569
|
-
/* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
|
|
570
|
-
/* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
|
|
571
|
-
/* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary font-['DM_Sans'] text-[20px] font-semibold", children: "Close this ticket?" }),
|
|
572
|
-
/* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary font-['DM_Sans'] text-[14px]", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
|
|
573
|
-
] }),
|
|
574
|
-
/* @__PURE__ */ jsx3(
|
|
575
|
-
Textarea,
|
|
576
|
-
{
|
|
577
|
-
value: resolution,
|
|
578
|
-
onChange: (e) => setResolution(e.target.value),
|
|
579
|
-
placeholder: "Resolution (optional)",
|
|
580
|
-
rows: 3,
|
|
581
|
-
maxLength: TICKET_TEXT_MAX_CHARS,
|
|
582
|
-
className: "mt-2"
|
|
583
|
-
}
|
|
584
|
-
),
|
|
585
|
-
/* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
|
|
586
|
-
/* @__PURE__ */ jsx3(
|
|
587
|
-
AlertDialogCancel,
|
|
588
|
-
{
|
|
589
|
-
disabled: busy,
|
|
590
|
-
className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
|
|
591
|
-
children: "Cancel"
|
|
592
|
-
}
|
|
593
|
-
),
|
|
594
|
-
/* @__PURE__ */ jsx3(
|
|
595
|
-
AlertDialogAction,
|
|
596
|
-
{
|
|
597
|
-
onClick: () => void confirmClose(),
|
|
598
|
-
disabled: busy,
|
|
599
|
-
className: "bg-ods-error hover:bg-ods-error-hover text-white",
|
|
600
|
-
children: "Close ticket"
|
|
601
|
-
}
|
|
602
|
-
)
|
|
603
|
-
] })
|
|
604
|
-
] }) })
|
|
605
|
-
] });
|
|
606
|
-
}
|
|
607
624
|
function ReplyFailureBanner({
|
|
608
625
|
error,
|
|
609
626
|
onDismiss
|
|
610
627
|
}) {
|
|
611
|
-
return /* @__PURE__ */
|
|
628
|
+
return /* @__PURE__ */ jsxs3(
|
|
612
629
|
"div",
|
|
613
630
|
{
|
|
614
631
|
role: "status",
|
|
615
632
|
"aria-live": "polite",
|
|
616
633
|
className: "mb-3 flex items-start gap-3 rounded-md border border-ods-attention-red-error bg-ods-attention-red-error-secondary px-3 py-2 text-sm text-ods-attention-red-error",
|
|
617
634
|
children: [
|
|
618
|
-
/* @__PURE__ */
|
|
619
|
-
/* @__PURE__ */
|
|
635
|
+
/* @__PURE__ */ jsx4("span", { className: "font-medium leading-snug", children: error.message }),
|
|
636
|
+
/* @__PURE__ */ jsx4(
|
|
620
637
|
Button,
|
|
621
638
|
{
|
|
622
639
|
type: "button",
|
|
@@ -637,10 +654,10 @@ function AssignedAgentRow({
|
|
|
637
654
|
const trimmedName = assignedOwner?.name?.trim() || null;
|
|
638
655
|
const emailFallback = assignedOwner?.email?.trim() || null;
|
|
639
656
|
const displayLabel = trimmedName ?? (emailFallback ? emailFallback.split("@")[0] : null);
|
|
640
|
-
return /* @__PURE__ */
|
|
641
|
-
/* @__PURE__ */
|
|
642
|
-
displayLabel ? /* @__PURE__ */
|
|
643
|
-
/* @__PURE__ */
|
|
657
|
+
return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
658
|
+
/* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary uppercase tracking-wider font-medium", children: "Assigned to" }),
|
|
659
|
+
displayLabel ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1.5 text-ods-text-primary font-medium", children: [
|
|
660
|
+
/* @__PURE__ */ jsx4(
|
|
644
661
|
SquareAvatar,
|
|
645
662
|
{
|
|
646
663
|
size: "sm",
|
|
@@ -651,12 +668,12 @@ function AssignedAgentRow({
|
|
|
651
668
|
}
|
|
652
669
|
),
|
|
653
670
|
displayLabel
|
|
654
|
-
] }) : /* @__PURE__ */
|
|
671
|
+
] }) : /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
|
|
655
672
|
] });
|
|
656
673
|
}
|
|
657
674
|
|
|
658
675
|
// src/components/tickets/ticket-row.tsx
|
|
659
|
-
import { jsx as
|
|
676
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
660
677
|
function TicketRow({
|
|
661
678
|
ticket,
|
|
662
679
|
expanded,
|
|
@@ -670,7 +687,7 @@ function TicketRow({
|
|
|
670
687
|
}) {
|
|
671
688
|
const optimistic = isOptimistic(ticket);
|
|
672
689
|
const rowRef = useRef(null);
|
|
673
|
-
const handleClick =
|
|
690
|
+
const handleClick = useCallback2(() => {
|
|
674
691
|
onToggle(ticket.id);
|
|
675
692
|
scrollElementIntoView(rowRef.current, {
|
|
676
693
|
adjustTargetY: (raw) => {
|
|
@@ -706,13 +723,13 @@ function TicketRow({
|
|
|
706
723
|
// when the task exists but its status hasn't synced yet.
|
|
707
724
|
linkedTaskLabel: ticket.clickup ? ticket.clickup.status ? ticket.clickup.status.replace(/\b\w/g, (c) => c.toUpperCase()) : "Linked work" : void 0
|
|
708
725
|
};
|
|
709
|
-
return /* @__PURE__ */
|
|
726
|
+
return /* @__PURE__ */ jsx5("div", { ref: rowRef, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs4(
|
|
710
727
|
Collapsible,
|
|
711
728
|
{
|
|
712
729
|
open: expanded && !optimistic,
|
|
713
730
|
className: "border-b border-ods-border last:border-b-0",
|
|
714
731
|
children: [
|
|
715
|
-
/* @__PURE__ */
|
|
732
|
+
/* @__PURE__ */ jsx5(
|
|
716
733
|
ChatTicketItem,
|
|
717
734
|
{
|
|
718
735
|
ticket: tileData,
|
|
@@ -721,12 +738,12 @@ function TicketRow({
|
|
|
721
738
|
"aria-controls": `ticket-drawer-${ticket.id}`
|
|
722
739
|
}
|
|
723
740
|
),
|
|
724
|
-
/* @__PURE__ */
|
|
741
|
+
/* @__PURE__ */ jsx5(
|
|
725
742
|
CollapsibleContent2,
|
|
726
743
|
{
|
|
727
744
|
id: `ticket-drawer-${ticket.id}`,
|
|
728
745
|
className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
|
|
729
|
-
children: /* @__PURE__ */
|
|
746
|
+
children: /* @__PURE__ */ jsx5(
|
|
730
747
|
TicketDetailDrawer,
|
|
731
748
|
{
|
|
732
749
|
ticket,
|
|
@@ -758,6 +775,7 @@ function useTicketsList(filters) {
|
|
|
758
775
|
const pageSize = Math.max(1, Math.min(100, Math.floor(filters.pageSize ?? DEFAULT_PAGE_SIZE) || DEFAULT_PAGE_SIZE));
|
|
759
776
|
const enabled = !!customerEmail;
|
|
760
777
|
const identityKey = customerEmail || "anon";
|
|
778
|
+
const refetchInterval = filters.refetchInterval ?? false;
|
|
761
779
|
const query = useQuery2({
|
|
762
780
|
queryKey: ["tickets", "self", identityKey, search, statusFilter, page, pageSize],
|
|
763
781
|
enabled,
|
|
@@ -770,6 +788,10 @@ function useTicketsList(filters) {
|
|
|
770
788
|
gcTime: 0,
|
|
771
789
|
refetchOnMount: "always",
|
|
772
790
|
refetchOnWindowFocus: true,
|
|
791
|
+
// Live status: poll while the caller opts in (drawer open). Defaults to
|
|
792
|
+
// false. `refetchIntervalInBackground` stays false (the default) so polling
|
|
793
|
+
// pauses on a hidden tab — no wasted requests when the user tabs away.
|
|
794
|
+
refetchInterval,
|
|
773
795
|
queryFn: async () => {
|
|
774
796
|
const body = {
|
|
775
797
|
query: search,
|
|
@@ -831,7 +853,7 @@ function useTicketsList(filters) {
|
|
|
831
853
|
}
|
|
832
854
|
|
|
833
855
|
// src/components/tickets/hooks/use-ticket-actions.ts
|
|
834
|
-
import { useCallback as
|
|
856
|
+
import { useCallback as useCallback3, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
|
|
835
857
|
import { useQueryClient } from "@tanstack/react-query";
|
|
836
858
|
var TICKET_ACTION_ENDPOINT = "/api/chat/agent/ticket-action";
|
|
837
859
|
var REPLY_BANNER_CODES = /* @__PURE__ */ new Set([
|
|
@@ -848,14 +870,14 @@ function useTicketActions(options) {
|
|
|
848
870
|
const [isSubmittingForm, setIsSubmittingForm] = useState3(false);
|
|
849
871
|
const busyRowsRef = useRef2(/* @__PURE__ */ new Set());
|
|
850
872
|
const [busyRows, setBusyRows] = useState3(() => /* @__PURE__ */ new Set());
|
|
851
|
-
const setRowBusy =
|
|
873
|
+
const setRowBusy = useCallback3((id, busy) => {
|
|
852
874
|
if (busy) busyRowsRef.current.add(id);
|
|
853
875
|
else busyRowsRef.current.delete(id);
|
|
854
876
|
setBusyRows(new Set(busyRowsRef.current));
|
|
855
877
|
}, []);
|
|
856
|
-
const isRowBusy =
|
|
878
|
+
const isRowBusy = useCallback3((id) => busyRows.has(id), [busyRows]);
|
|
857
879
|
const [replyErrorByTicket, setReplyErrorByTicket] = useState3(() => /* @__PURE__ */ new Map());
|
|
858
|
-
const setReplyError =
|
|
880
|
+
const setReplyError = useCallback3(
|
|
859
881
|
(externalId, mapped) => {
|
|
860
882
|
setReplyErrorByTicket((prev) => {
|
|
861
883
|
const next = new Map(prev);
|
|
@@ -866,11 +888,11 @@ function useTicketActions(options) {
|
|
|
866
888
|
},
|
|
867
889
|
[]
|
|
868
890
|
);
|
|
869
|
-
const replyErrorFor =
|
|
891
|
+
const replyErrorFor = useCallback3(
|
|
870
892
|
(externalId) => replyErrorByTicket.get(externalId) ?? null,
|
|
871
893
|
[replyErrorByTicket]
|
|
872
894
|
);
|
|
873
|
-
const clearReplyError =
|
|
895
|
+
const clearReplyError = useCallback3(
|
|
874
896
|
(externalId) => setReplyError(externalId, null),
|
|
875
897
|
[setReplyError]
|
|
876
898
|
);
|
|
@@ -884,12 +906,12 @@ function useTicketActions(options) {
|
|
|
884
906
|
};
|
|
885
907
|
}, []);
|
|
886
908
|
const queueRef = useRef2(Promise.resolve());
|
|
887
|
-
const enqueue =
|
|
909
|
+
const enqueue = useCallback3((work) => {
|
|
888
910
|
const next = queueRef.current.then(work, work);
|
|
889
911
|
queueRef.current = next.catch(() => void 0);
|
|
890
912
|
return next;
|
|
891
913
|
}, []);
|
|
892
|
-
const executeTicketAction =
|
|
914
|
+
const executeTicketAction = useCallback3(
|
|
893
915
|
async (toolName, args) => {
|
|
894
916
|
const res = await embedAuthedFetch(TICKET_ACTION_ENDPOINT, {
|
|
895
917
|
method: "POST",
|
|
@@ -905,7 +927,7 @@ function useTicketActions(options) {
|
|
|
905
927
|
},
|
|
906
928
|
[]
|
|
907
929
|
);
|
|
908
|
-
const watchMirrorSync =
|
|
930
|
+
const watchMirrorSync = useCallback3(
|
|
909
931
|
(placeholderId, expectedTicketId) => {
|
|
910
932
|
const prior = watcherControllersRef.current.get(placeholderId);
|
|
911
933
|
if (prior) prior.abort();
|
|
@@ -952,7 +974,7 @@ function useTicketActions(options) {
|
|
|
952
974
|
[queryClient, removeOptimistic, toast2]
|
|
953
975
|
);
|
|
954
976
|
const lastUpdateErrorRef = useRef2(null);
|
|
955
|
-
const surfaceError =
|
|
977
|
+
const surfaceError = useCallback3(
|
|
956
978
|
(err, action) => {
|
|
957
979
|
const mapped = mapTicketActionError(err);
|
|
958
980
|
lastUpdateErrorRef.current = mapped;
|
|
@@ -966,7 +988,7 @@ function useTicketActions(options) {
|
|
|
966
988
|
},
|
|
967
989
|
[toast2, onSupportSystemDown]
|
|
968
990
|
);
|
|
969
|
-
const submitTicket =
|
|
991
|
+
const submitTicket = useCallback3(
|
|
970
992
|
async (input) => {
|
|
971
993
|
if (formInFlightRef.current) return false;
|
|
972
994
|
formInFlightRef.current = true;
|
|
@@ -1035,7 +1057,7 @@ function useTicketActions(options) {
|
|
|
1035
1057
|
surfaceError
|
|
1036
1058
|
]
|
|
1037
1059
|
);
|
|
1038
|
-
const updateTicket =
|
|
1060
|
+
const updateTicket = useCallback3(
|
|
1039
1061
|
async (ticket, serverArgs, successCopy, action) => {
|
|
1040
1062
|
if (busyRowsRef.current.has(ticket.id)) return false;
|
|
1041
1063
|
setRowBusy(ticket.id, true);
|
|
@@ -1081,7 +1103,7 @@ function useTicketActions(options) {
|
|
|
1081
1103
|
// and cascade-recreate addNote/closeTicket/etc.
|
|
1082
1104
|
[setRowBusy, enqueue, executeTicketAction, queryClient, toast2, surfaceError, removeTicketFromCache]
|
|
1083
1105
|
);
|
|
1084
|
-
const sendMessage =
|
|
1106
|
+
const sendMessage = useCallback3(
|
|
1085
1107
|
async (ticket, text, attachments) => {
|
|
1086
1108
|
const trimmed = text.trim();
|
|
1087
1109
|
const hasText = trimmed.length > 0;
|
|
@@ -1110,7 +1132,7 @@ function useTicketActions(options) {
|
|
|
1110
1132
|
},
|
|
1111
1133
|
[updateTicket, clearReplyError, setReplyError]
|
|
1112
1134
|
);
|
|
1113
|
-
const closeTicket =
|
|
1135
|
+
const closeTicket = useCallback3(
|
|
1114
1136
|
(ticket, resolution) => updateTicket(
|
|
1115
1137
|
ticket,
|
|
1116
1138
|
{
|
|
@@ -1122,7 +1144,7 @@ function useTicketActions(options) {
|
|
|
1122
1144
|
),
|
|
1123
1145
|
[updateTicket]
|
|
1124
1146
|
);
|
|
1125
|
-
const reopenTicket =
|
|
1147
|
+
const reopenTicket = useCallback3(
|
|
1126
1148
|
(ticket) => updateTicket(ticket, { status: "OPEN" }, TOAST_COPY.reopen_success, "reopen ticket"),
|
|
1127
1149
|
[updateTicket]
|
|
1128
1150
|
);
|
|
@@ -1274,14 +1296,14 @@ function resolveErrorCode(bodyCode, status) {
|
|
|
1274
1296
|
}
|
|
1275
1297
|
|
|
1276
1298
|
// src/components/tickets/ticket-center.tsx
|
|
1277
|
-
import { jsx as
|
|
1299
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1278
1300
|
function TicketCenter({ toast: toast2 = toast } = {}) {
|
|
1279
1301
|
const identity = useChatIdentity();
|
|
1280
1302
|
if (identity.isLoading) {
|
|
1281
|
-
return /* @__PURE__ */
|
|
1303
|
+
return /* @__PURE__ */ jsx6(TicketCenterSkeleton, {});
|
|
1282
1304
|
}
|
|
1283
1305
|
if (identity.authTier === "anon" || !identity.user?.email) {
|
|
1284
|
-
return /* @__PURE__ */
|
|
1306
|
+
return /* @__PURE__ */ jsx6(
|
|
1285
1307
|
EmptyState,
|
|
1286
1308
|
{
|
|
1287
1309
|
type: "generic",
|
|
@@ -1291,7 +1313,7 @@ function TicketCenter({ toast: toast2 = toast } = {}) {
|
|
|
1291
1313
|
}
|
|
1292
1314
|
);
|
|
1293
1315
|
}
|
|
1294
|
-
return /* @__PURE__ */
|
|
1316
|
+
return /* @__PURE__ */ jsx6(
|
|
1295
1317
|
TicketCenterAuthed,
|
|
1296
1318
|
{
|
|
1297
1319
|
toast: toast2,
|
|
@@ -1310,14 +1332,14 @@ function TicketCenterAuthed({
|
|
|
1310
1332
|
const [optimisticTickets, setOptimisticTickets] = useState4([]);
|
|
1311
1333
|
const [expandedTicketId, setExpandedTicketId] = useState4(null);
|
|
1312
1334
|
const [supportSystemDown, setSupportSystemDown] = useState4(false);
|
|
1313
|
-
const prependOptimistic =
|
|
1335
|
+
const prependOptimistic = useCallback4((placeholder) => {
|
|
1314
1336
|
setOptimisticTickets((prev) => [placeholder, ...prev]);
|
|
1315
1337
|
}, []);
|
|
1316
|
-
const removeOptimistic =
|
|
1338
|
+
const removeOptimistic = useCallback4((placeholderId) => {
|
|
1317
1339
|
setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
|
|
1318
1340
|
setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
|
|
1319
1341
|
}, []);
|
|
1320
|
-
const removeTicketFromCache =
|
|
1342
|
+
const removeTicketFromCache = useCallback4(
|
|
1321
1343
|
(ticketId) => {
|
|
1322
1344
|
queryClient.setQueriesData(
|
|
1323
1345
|
{ queryKey: ["tickets"] },
|
|
@@ -1334,12 +1356,12 @@ function TicketCenterAuthed({
|
|
|
1334
1356
|
toast: toast2,
|
|
1335
1357
|
onSupportSystemDown: () => setSupportSystemDown(true)
|
|
1336
1358
|
});
|
|
1337
|
-
const toggleRow =
|
|
1359
|
+
const toggleRow = useCallback4((id) => {
|
|
1338
1360
|
setExpandedTicketId((prev) => prev === id ? null : id);
|
|
1339
1361
|
}, []);
|
|
1340
1362
|
const merged = [...optimisticTickets, ...tickets];
|
|
1341
|
-
return /* @__PURE__ */
|
|
1342
|
-
/* @__PURE__ */
|
|
1363
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
|
|
1364
|
+
/* @__PURE__ */ jsx6(
|
|
1343
1365
|
TicketOpenForm,
|
|
1344
1366
|
{
|
|
1345
1367
|
onSubmit: (input) => actions.submitTicket(input),
|
|
@@ -1347,15 +1369,15 @@ function TicketCenterAuthed({
|
|
|
1347
1369
|
supportSystemDown
|
|
1348
1370
|
}
|
|
1349
1371
|
),
|
|
1350
|
-
/* @__PURE__ */
|
|
1351
|
-
/* @__PURE__ */
|
|
1352
|
-
/* @__PURE__ */
|
|
1353
|
-
/* @__PURE__ */
|
|
1354
|
-
lastUpdatedAt && /* @__PURE__ */
|
|
1372
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
|
|
1373
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3", children: [
|
|
1374
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs font-medium text-ods-text-secondary uppercase tracking-wider", children: "Your Current Tickets" }),
|
|
1375
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3 text-xs text-ods-text-secondary", children: [
|
|
1376
|
+
lastUpdatedAt && /* @__PURE__ */ jsxs5("span", { children: [
|
|
1355
1377
|
"Updated ",
|
|
1356
1378
|
formatRelativeTime(new Date(lastUpdatedAt))
|
|
1357
1379
|
] }),
|
|
1358
|
-
/* @__PURE__ */
|
|
1380
|
+
/* @__PURE__ */ jsx6(
|
|
1359
1381
|
Button,
|
|
1360
1382
|
{
|
|
1361
1383
|
type: "button",
|
|
@@ -1364,12 +1386,12 @@ function TicketCenterAuthed({
|
|
|
1364
1386
|
onClick: refetch,
|
|
1365
1387
|
disabled: isFetching,
|
|
1366
1388
|
"aria-label": "Refresh ticket list",
|
|
1367
|
-
leftIcon: /* @__PURE__ */
|
|
1389
|
+
leftIcon: /* @__PURE__ */ jsx6(RefreshCw, { className: "h-4 w-4" })
|
|
1368
1390
|
}
|
|
1369
1391
|
)
|
|
1370
1392
|
] })
|
|
1371
1393
|
] }),
|
|
1372
|
-
isLoading ? /* @__PURE__ */
|
|
1394
|
+
isLoading ? /* @__PURE__ */ jsx6(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx6(Card, { className: "p-6", children: /* @__PURE__ */ jsx6(
|
|
1373
1395
|
EmptyState,
|
|
1374
1396
|
{
|
|
1375
1397
|
type: "generic",
|
|
@@ -1377,7 +1399,7 @@ function TicketCenterAuthed({
|
|
|
1377
1399
|
description: "Open one above to start the conversation.",
|
|
1378
1400
|
showCTA: false
|
|
1379
1401
|
}
|
|
1380
|
-
) }) : /* @__PURE__ */
|
|
1402
|
+
) }) : /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx6(
|
|
1381
1403
|
TicketRow,
|
|
1382
1404
|
{
|
|
1383
1405
|
ticket,
|
|
@@ -1396,34 +1418,34 @@ function TicketCenterAuthed({
|
|
|
1396
1418
|
] });
|
|
1397
1419
|
}
|
|
1398
1420
|
function TicketCenterSkeleton() {
|
|
1399
|
-
return /* @__PURE__ */
|
|
1400
|
-
/* @__PURE__ */
|
|
1401
|
-
/* @__PURE__ */
|
|
1402
|
-
/* @__PURE__ */
|
|
1403
|
-
/* @__PURE__ */
|
|
1421
|
+
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
|
|
1422
|
+
/* @__PURE__ */ jsxs5(Card, { className: "p-6", children: [
|
|
1423
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-7 w-48 mb-4" }),
|
|
1424
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-10 w-full mb-3" }),
|
|
1425
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-24 w-full" })
|
|
1404
1426
|
] }),
|
|
1405
|
-
/* @__PURE__ */
|
|
1427
|
+
/* @__PURE__ */ jsx6(TicketListSkeleton, {})
|
|
1406
1428
|
] });
|
|
1407
1429
|
}
|
|
1408
1430
|
function TicketListSkeleton() {
|
|
1409
|
-
return /* @__PURE__ */
|
|
1410
|
-
/* @__PURE__ */
|
|
1411
|
-
/* @__PURE__ */
|
|
1412
|
-
/* @__PURE__ */
|
|
1431
|
+
return /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsxs5("div", { className: "h-20 px-4 flex items-center gap-4 border-b border-ods-border last:border-b-0", children: [
|
|
1432
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex-1 flex flex-col gap-2", children: [
|
|
1433
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-4 w-2/3" }),
|
|
1434
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-3 w-full" })
|
|
1413
1435
|
] }),
|
|
1414
|
-
/* @__PURE__ */
|
|
1415
|
-
/* @__PURE__ */
|
|
1436
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-20" }),
|
|
1437
|
+
/* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-16" })
|
|
1416
1438
|
] }, i)) });
|
|
1417
1439
|
}
|
|
1418
1440
|
|
|
1419
1441
|
// src/components/tickets/help-center-list.tsx
|
|
1420
|
-
import { useCallback as
|
|
1442
|
+
import { useCallback as useCallback6, useState as useState6 } from "react";
|
|
1421
1443
|
import { useQueryClient as useQueryClient3 } from "@tanstack/react-query";
|
|
1422
1444
|
init_unified_pagination();
|
|
1423
1445
|
|
|
1424
1446
|
// src/components/tickets/help-center-card.tsx
|
|
1425
|
-
import { useCallback as
|
|
1426
|
-
import { Fragment, jsx as
|
|
1447
|
+
import { useCallback as useCallback5, useEffect as useEffect2, useRef as useRef3 } from "react";
|
|
1448
|
+
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1427
1449
|
var STICKY_HEADER_OFFSET_PX = 96;
|
|
1428
1450
|
function HelpCenterCard({
|
|
1429
1451
|
ticket,
|
|
@@ -1448,25 +1470,20 @@ function HelpCenterCard({
|
|
|
1448
1470
|
const isExpandable = !optimistic;
|
|
1449
1471
|
const isExpanded = expanded && isExpandable;
|
|
1450
1472
|
const rowRef = useRef3(null);
|
|
1451
|
-
const handleClick =
|
|
1473
|
+
const handleClick = useCallback5(() => {
|
|
1452
1474
|
onToggle(ticket.id);
|
|
1453
|
-
scrollElementIntoView(rowRef.current, {
|
|
1454
|
-
headerOffset: STICKY_HEADER_OFFSET_PX,
|
|
1455
|
-
adjustTargetY: (raw) => {
|
|
1456
|
-
if (!rowRef.current) return raw;
|
|
1457
|
-
const expandedDrawer = document.querySelector(
|
|
1458
|
-
'div[id^="help-center-drawer-"]'
|
|
1459
|
-
);
|
|
1460
|
-
if (!(expandedDrawer instanceof HTMLElement)) return raw;
|
|
1461
|
-
const drawerRect = expandedDrawer.getBoundingClientRect();
|
|
1462
|
-
const myRect = rowRef.current.getBoundingClientRect();
|
|
1463
|
-
if (drawerRect.bottom > myRect.top) return raw;
|
|
1464
|
-
return raw - drawerRect.height;
|
|
1465
|
-
}
|
|
1466
|
-
});
|
|
1467
1475
|
}, [onToggle, ticket.id]);
|
|
1468
|
-
|
|
1469
|
-
|
|
1476
|
+
useEffect2(() => {
|
|
1477
|
+
if (!isExpanded) return;
|
|
1478
|
+
const raf = requestAnimationFrame(() => {
|
|
1479
|
+
scrollElementIntoView(rowRef.current, {
|
|
1480
|
+
headerOffset: STICKY_HEADER_OFFSET_PX
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
return () => cancelAnimationFrame(raf);
|
|
1484
|
+
}, [isExpanded]);
|
|
1485
|
+
const rightBadges = /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1486
|
+
/* @__PURE__ */ jsx7(
|
|
1470
1487
|
StatusBadge,
|
|
1471
1488
|
{
|
|
1472
1489
|
text: rawStatus,
|
|
@@ -1475,7 +1492,7 @@ function HelpCenterCard({
|
|
|
1475
1492
|
className: "border border-ods-border"
|
|
1476
1493
|
}
|
|
1477
1494
|
),
|
|
1478
|
-
priority && /* @__PURE__ */
|
|
1495
|
+
priority && /* @__PURE__ */ jsx7(
|
|
1479
1496
|
StatusBadge,
|
|
1480
1497
|
{
|
|
1481
1498
|
text: priority,
|
|
@@ -1485,7 +1502,7 @@ function HelpCenterCard({
|
|
|
1485
1502
|
}
|
|
1486
1503
|
)
|
|
1487
1504
|
] });
|
|
1488
|
-
return /* @__PURE__ */
|
|
1505
|
+
return /* @__PURE__ */ jsxs6(
|
|
1489
1506
|
"div",
|
|
1490
1507
|
{
|
|
1491
1508
|
ref: rowRef,
|
|
@@ -1493,7 +1510,7 @@ function HelpCenterCard({
|
|
|
1493
1510
|
className: `border-b border-ods-border last:border-b-0 ${optimistic ? "opacity-60" : ""}`,
|
|
1494
1511
|
"aria-busy": optimistic || void 0,
|
|
1495
1512
|
children: [
|
|
1496
|
-
/* @__PURE__ */
|
|
1513
|
+
/* @__PURE__ */ jsx7(
|
|
1497
1514
|
"button",
|
|
1498
1515
|
{
|
|
1499
1516
|
type: "button",
|
|
@@ -1502,7 +1519,7 @@ function HelpCenterCard({
|
|
|
1502
1519
|
"aria-expanded": isExpandable ? isExpanded : void 0,
|
|
1503
1520
|
"aria-controls": isExpanded ? `help-center-drawer-${ticket.id}` : void 0,
|
|
1504
1521
|
className: "w-full text-left p-[12px] md:p-[16px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-inset disabled:cursor-default",
|
|
1505
|
-
children: /* @__PURE__ */
|
|
1522
|
+
children: /* @__PURE__ */ jsx7(
|
|
1506
1523
|
DevCardRowContent,
|
|
1507
1524
|
{
|
|
1508
1525
|
title,
|
|
@@ -1514,7 +1531,7 @@ function HelpCenterCard({
|
|
|
1514
1531
|
)
|
|
1515
1532
|
}
|
|
1516
1533
|
),
|
|
1517
|
-
isExpanded && /* @__PURE__ */
|
|
1534
|
+
isExpanded && /* @__PURE__ */ jsx7("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx7(
|
|
1518
1535
|
TicketDetailDrawer,
|
|
1519
1536
|
{
|
|
1520
1537
|
ticket,
|
|
@@ -1540,31 +1557,31 @@ function mapPriorityScheme(priority) {
|
|
|
1540
1557
|
|
|
1541
1558
|
// src/components/tickets/help-center-create-form.tsx
|
|
1542
1559
|
import { useState as useState5 } from "react";
|
|
1543
|
-
import { jsx as
|
|
1560
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1544
1561
|
var SUBJECT_MAX_CHARS = 200;
|
|
1545
1562
|
function HelpCenterCreateFormSkeleton() {
|
|
1546
|
-
return /* @__PURE__ */
|
|
1547
|
-
/* @__PURE__ */
|
|
1548
|
-
/* @__PURE__ */
|
|
1549
|
-
/* @__PURE__ */
|
|
1550
|
-
/* @__PURE__ */
|
|
1551
|
-
/* @__PURE__ */
|
|
1552
|
-
/* @__PURE__ */
|
|
1553
|
-
/* @__PURE__ */
|
|
1554
|
-
/* @__PURE__ */
|
|
1555
|
-
/* @__PURE__ */
|
|
1563
|
+
return /* @__PURE__ */ jsxs7("div", { className: "h-full flex flex-col border border-ods-border rounded-2xl md:rounded-3xl p-6 md:p-8 lg:p-10", children: [
|
|
1564
|
+
/* @__PURE__ */ jsx8("div", { className: "mb-6 md:mb-8", children: /* @__PURE__ */ jsx8("div", { className: "h-10 w-72 bg-ods-border rounded animate-pulse mb-3 md:mb-4" }) }),
|
|
1565
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow space-y-4 md:space-y-6", children: [
|
|
1566
|
+
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1567
|
+
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1568
|
+
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1569
|
+
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1570
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
|
|
1571
|
+
/* @__PURE__ */ jsx8("div", { className: "h-[27px] w-20 bg-ods-border rounded animate-pulse mb-1" }),
|
|
1572
|
+
/* @__PURE__ */ jsx8("div", { className: "h-12 w-full bg-ods-border rounded animate-pulse" })
|
|
1556
1573
|
] }),
|
|
1557
|
-
/* @__PURE__ */
|
|
1558
|
-
/* @__PURE__ */
|
|
1559
|
-
/* @__PURE__ */
|
|
1574
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow", children: [
|
|
1575
|
+
/* @__PURE__ */ jsx8("div", { className: "h-[27px] w-32 bg-ods-border rounded animate-pulse mb-1" }),
|
|
1576
|
+
/* @__PURE__ */ jsx8("div", { className: "h-24 w-full bg-ods-border rounded animate-pulse flex-grow" })
|
|
1560
1577
|
] }),
|
|
1561
|
-
/* @__PURE__ */
|
|
1562
|
-
/* @__PURE__ */
|
|
1563
|
-
/* @__PURE__ */
|
|
1578
|
+
/* @__PURE__ */ jsx8("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1579
|
+
/* @__PURE__ */ jsx8("div", { className: "h-7 w-7 bg-ods-border rounded animate-pulse shrink-0" }),
|
|
1580
|
+
/* @__PURE__ */ jsx8("div", { className: "h-4 w-40 bg-ods-border rounded animate-pulse" })
|
|
1564
1581
|
] }) }),
|
|
1565
|
-
/* @__PURE__ */
|
|
1566
|
-
/* @__PURE__ */
|
|
1567
|
-
/* @__PURE__ */
|
|
1582
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col md:flex-row gap-4 md:gap-6 items-center justify-end w-full pt-2 mt-auto", children: [
|
|
1583
|
+
/* @__PURE__ */ jsx8("div", { className: "h-4 w-72 bg-ods-border rounded animate-pulse" }),
|
|
1584
|
+
/* @__PURE__ */ jsx8("div", { className: "h-12 w-32 bg-ods-border rounded animate-pulse" })
|
|
1568
1585
|
] })
|
|
1569
1586
|
] })
|
|
1570
1587
|
] });
|
|
@@ -1577,12 +1594,12 @@ function HelpCenterCreateForm({
|
|
|
1577
1594
|
}) {
|
|
1578
1595
|
const [subject, setSubject] = useState5("");
|
|
1579
1596
|
const [subjectError, setSubjectError] = useState5(null);
|
|
1580
|
-
const subjectField = /* @__PURE__ */
|
|
1581
|
-
/* @__PURE__ */
|
|
1597
|
+
const subjectField = /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
|
|
1598
|
+
/* @__PURE__ */ jsxs7(Label, { htmlFor: "help-center-subject", children: [
|
|
1582
1599
|
"Subject",
|
|
1583
|
-
/* @__PURE__ */
|
|
1600
|
+
/* @__PURE__ */ jsx8("span", { className: "text-ods-accent", children: "*" })
|
|
1584
1601
|
] }),
|
|
1585
|
-
/* @__PURE__ */
|
|
1602
|
+
/* @__PURE__ */ jsx8(
|
|
1586
1603
|
Input,
|
|
1587
1604
|
{
|
|
1588
1605
|
id: "help-center-subject",
|
|
@@ -1600,7 +1617,7 @@ function HelpCenterCreateForm({
|
|
|
1600
1617
|
className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12"
|
|
1601
1618
|
}
|
|
1602
1619
|
),
|
|
1603
|
-
subjectError && /* @__PURE__ */
|
|
1620
|
+
subjectError && /* @__PURE__ */ jsx8(
|
|
1604
1621
|
"span",
|
|
1605
1622
|
{
|
|
1606
1623
|
id: "help-center-subject-error",
|
|
@@ -1609,7 +1626,7 @@ function HelpCenterCreateForm({
|
|
|
1609
1626
|
}
|
|
1610
1627
|
)
|
|
1611
1628
|
] });
|
|
1612
|
-
return /* @__PURE__ */
|
|
1629
|
+
return /* @__PURE__ */ jsx8(
|
|
1613
1630
|
ContactForm,
|
|
1614
1631
|
{
|
|
1615
1632
|
title: "Open a new ticket",
|
|
@@ -1646,7 +1663,7 @@ function HelpCenterCreateForm({
|
|
|
1646
1663
|
}
|
|
1647
1664
|
|
|
1648
1665
|
// src/components/tickets/help-center-list.tsx
|
|
1649
|
-
import { jsx as
|
|
1666
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1650
1667
|
function HelpCenterList({ toast: toast2 = toast } = {}) {
|
|
1651
1668
|
const identity = useChatIdentity();
|
|
1652
1669
|
const searchParams = useSearchParams();
|
|
@@ -1654,13 +1671,14 @@ function HelpCenterList({ toast: toast2 = toast } = {}) {
|
|
|
1654
1671
|
const pathname = usePathname();
|
|
1655
1672
|
const search = searchParams.get("search") || "";
|
|
1656
1673
|
const status = searchParams.get("status") || "all";
|
|
1674
|
+
const ticketParam = searchParams.get("ticket") || "";
|
|
1657
1675
|
const rawPage = Number(searchParams.get("page"));
|
|
1658
1676
|
const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
|
|
1659
1677
|
if (identity.isLoading) {
|
|
1660
|
-
return /* @__PURE__ */
|
|
1678
|
+
return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", preControls: /* @__PURE__ */ jsx9(HelpCenterCreateFormSkeleton, {}), children: /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) });
|
|
1661
1679
|
}
|
|
1662
1680
|
if (identity.authTier === "anon" || !identity.user?.email) {
|
|
1663
|
-
return /* @__PURE__ */
|
|
1681
|
+
return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", children: /* @__PURE__ */ jsx9(
|
|
1664
1682
|
EmptyState,
|
|
1665
1683
|
{
|
|
1666
1684
|
type: "generic",
|
|
@@ -1672,12 +1690,13 @@ function HelpCenterList({ toast: toast2 = toast } = {}) {
|
|
|
1672
1690
|
}
|
|
1673
1691
|
const sessionName = [identity.user?.firstName, identity.user?.lastName].filter(Boolean).join(" ").trim() || identity.user?.email?.split("@")[0] || "Customer";
|
|
1674
1692
|
const sessionEmail = identity.user.email;
|
|
1675
|
-
return /* @__PURE__ */
|
|
1693
|
+
return /* @__PURE__ */ jsx9(
|
|
1676
1694
|
HelpCenterListAuthed,
|
|
1677
1695
|
{
|
|
1678
1696
|
search,
|
|
1679
1697
|
status,
|
|
1680
1698
|
page,
|
|
1699
|
+
ticketParam,
|
|
1681
1700
|
searchParams,
|
|
1682
1701
|
router,
|
|
1683
1702
|
pathname,
|
|
@@ -1691,6 +1710,7 @@ function HelpCenterListAuthed({
|
|
|
1691
1710
|
search,
|
|
1692
1711
|
status,
|
|
1693
1712
|
page,
|
|
1713
|
+
ticketParam,
|
|
1694
1714
|
searchParams,
|
|
1695
1715
|
router,
|
|
1696
1716
|
pathname,
|
|
@@ -1699,6 +1719,18 @@ function HelpCenterListAuthed({
|
|
|
1699
1719
|
sessionEmail
|
|
1700
1720
|
}) {
|
|
1701
1721
|
const queryClient = useQueryClient3();
|
|
1722
|
+
const [optimisticTickets, setOptimisticTickets] = useState6([]);
|
|
1723
|
+
const [supportSystemDown, setSupportSystemDown] = useState6(false);
|
|
1724
|
+
const setOpenTicket = useCallback6(
|
|
1725
|
+
(externalId) => {
|
|
1726
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
1727
|
+
if (externalId) params.set("ticket", externalId);
|
|
1728
|
+
else params.delete("ticket");
|
|
1729
|
+
const qs = params.toString();
|
|
1730
|
+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
1731
|
+
},
|
|
1732
|
+
[searchParams, router, pathname]
|
|
1733
|
+
);
|
|
1702
1734
|
const { tickets, isLoading, isFetching, error, refetch, totalPages } = useTicketsList({
|
|
1703
1735
|
// `sessionEmail` is drilled in from the parent — see the same
|
|
1704
1736
|
// pattern + race-cause rationale documented in
|
|
@@ -1709,19 +1741,21 @@ function HelpCenterListAuthed({
|
|
|
1709
1741
|
customerEmail: sessionEmail,
|
|
1710
1742
|
search,
|
|
1711
1743
|
status,
|
|
1712
|
-
page
|
|
1744
|
+
page,
|
|
1745
|
+
// Live status: while a drawer is open, poll so an out-of-band HubSpot
|
|
1746
|
+
// status change (e.g. agent closes the ticket) flips the badge +
|
|
1747
|
+
// open/reopen affordance within one interval. Idle (no drawer) → no poll.
|
|
1748
|
+
// `ticketParam` (the open ticket's external_id) is the open signal.
|
|
1749
|
+
refetchInterval: ticketParam ? TICKET_LIVE_POLL_MS : false
|
|
1713
1750
|
});
|
|
1714
|
-
const
|
|
1715
|
-
const
|
|
1716
|
-
const [supportSystemDown, setSupportSystemDown] = useState6(false);
|
|
1717
|
-
const prependOptimistic = useCallback5((placeholder) => {
|
|
1751
|
+
const expandedTicketId = ticketParam && tickets.find((t) => t.external_id === ticketParam)?.id || null;
|
|
1752
|
+
const prependOptimistic = useCallback6((placeholder) => {
|
|
1718
1753
|
setOptimisticTickets((prev) => [placeholder, ...prev]);
|
|
1719
1754
|
}, []);
|
|
1720
|
-
const removeOptimistic =
|
|
1755
|
+
const removeOptimistic = useCallback6((placeholderId) => {
|
|
1721
1756
|
setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
|
|
1722
|
-
setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
|
|
1723
1757
|
}, []);
|
|
1724
|
-
const removeTicketFromCache =
|
|
1758
|
+
const removeTicketFromCache = useCallback6(
|
|
1725
1759
|
(ticketId) => {
|
|
1726
1760
|
queryClient.setQueriesData(
|
|
1727
1761
|
{ queryKey: ["tickets"] },
|
|
@@ -1732,7 +1766,6 @@ function HelpCenterListAuthed({
|
|
|
1732
1766
|
return { ...prev, tickets: nextTickets };
|
|
1733
1767
|
}
|
|
1734
1768
|
);
|
|
1735
|
-
setExpandedTicketId((prev) => prev === ticketId ? null : prev);
|
|
1736
1769
|
},
|
|
1737
1770
|
[queryClient]
|
|
1738
1771
|
);
|
|
@@ -1743,13 +1776,18 @@ function HelpCenterListAuthed({
|
|
|
1743
1776
|
toast: toast2,
|
|
1744
1777
|
onSupportSystemDown: () => setSupportSystemDown(true)
|
|
1745
1778
|
});
|
|
1746
|
-
const toggleRow =
|
|
1747
|
-
|
|
1748
|
-
|
|
1779
|
+
const toggleRow = useCallback6(
|
|
1780
|
+
(id) => {
|
|
1781
|
+
const t = tickets.find((x) => x.id === id);
|
|
1782
|
+
if (!t?.external_id) return;
|
|
1783
|
+
setOpenTicket(t.external_id === ticketParam ? null : t.external_id);
|
|
1784
|
+
},
|
|
1785
|
+
[tickets, ticketParam, setOpenTicket]
|
|
1786
|
+
);
|
|
1749
1787
|
const merged = [...optimisticTickets, ...tickets];
|
|
1750
1788
|
const hasActiveFilters = search !== "" || status !== "" && status !== "all";
|
|
1751
1789
|
const hasResults = merged.length > 0;
|
|
1752
|
-
const form = /* @__PURE__ */
|
|
1790
|
+
const form = /* @__PURE__ */ jsx9(
|
|
1753
1791
|
HelpCenterCreateForm,
|
|
1754
1792
|
{
|
|
1755
1793
|
actions,
|
|
@@ -1758,22 +1796,22 @@ function HelpCenterListAuthed({
|
|
|
1758
1796
|
supportSystemDown
|
|
1759
1797
|
}
|
|
1760
1798
|
);
|
|
1761
|
-
const body = /* @__PURE__ */
|
|
1762
|
-
error && /* @__PURE__ */
|
|
1763
|
-
/* @__PURE__ */
|
|
1799
|
+
const body = /* @__PURE__ */ jsxs8("div", { className: "w-full flex flex-col gap-[40px]", children: [
|
|
1800
|
+
error && /* @__PURE__ */ jsxs8("div", { className: "bg-ods-card border border-ods-border rounded-[6px] p-[40px] text-center w-full flex flex-col items-center gap-3", children: [
|
|
1801
|
+
/* @__PURE__ */ jsxs8("p", { className: "text-ods-error text-base", children: [
|
|
1764
1802
|
"Couldn\u2019t load your tickets. ",
|
|
1765
1803
|
error.message
|
|
1766
1804
|
] }),
|
|
1767
|
-
/* @__PURE__ */
|
|
1805
|
+
/* @__PURE__ */ jsx9(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
|
|
1768
1806
|
] }),
|
|
1769
|
-
!error && /* @__PURE__ */
|
|
1807
|
+
!error && /* @__PURE__ */ jsx9("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
|
|
1770
1808
|
// Bridge state — background refetch in flight and the
|
|
1771
1809
|
// optimistic placeholder was just removed by the mutation
|
|
1772
1810
|
// callback. Without this branch "No tickets yet" would flash
|
|
1773
1811
|
// for ~50ms between `removeOptimistic` and the server
|
|
1774
1812
|
// response landing.
|
|
1775
|
-
/* @__PURE__ */
|
|
1776
|
-
) : !hasResults ? hasActiveFilters ? /* @__PURE__ */
|
|
1813
|
+
/* @__PURE__ */ jsx9(DevCardRowSkeletonList, { rows: 1 })
|
|
1814
|
+
) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx9(
|
|
1777
1815
|
EmptyState,
|
|
1778
1816
|
{
|
|
1779
1817
|
type: "search",
|
|
@@ -1788,7 +1826,7 @@ function HelpCenterListAuthed({
|
|
|
1788
1826
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
1789
1827
|
}
|
|
1790
1828
|
}
|
|
1791
|
-
) : /* @__PURE__ */
|
|
1829
|
+
) : /* @__PURE__ */ jsx9(
|
|
1792
1830
|
EmptyState,
|
|
1793
1831
|
{
|
|
1794
1832
|
type: "generic",
|
|
@@ -1805,7 +1843,7 @@ function HelpCenterListAuthed({
|
|
|
1805
1843
|
// hidden) instead of bubbling up to the window. `clip`
|
|
1806
1844
|
// keeps the visual clip but NOT the scroll-container
|
|
1807
1845
|
// status, so click-to-scroll actually moves the page.
|
|
1808
|
-
/* @__PURE__ */
|
|
1846
|
+
/* @__PURE__ */ jsx9("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full", children: merged.map((ticket) => /* @__PURE__ */ jsx9(
|
|
1809
1847
|
HelpCenterCard,
|
|
1810
1848
|
{
|
|
1811
1849
|
ticket,
|
|
@@ -1816,16 +1854,16 @@ function HelpCenterListAuthed({
|
|
|
1816
1854
|
onSendMessage: actions.sendMessage,
|
|
1817
1855
|
onClose: actions.closeTicket,
|
|
1818
1856
|
onReopen: actions.reopenTicket,
|
|
1819
|
-
onActionCollapsed: () =>
|
|
1857
|
+
onActionCollapsed: () => setOpenTicket(null),
|
|
1820
1858
|
replyError: actions.replyErrorFor(ticket.external_id),
|
|
1821
1859
|
onClearReplyError: () => actions.clearReplyError(ticket.external_id)
|
|
1822
1860
|
},
|
|
1823
1861
|
ticket.id
|
|
1824
1862
|
)) })
|
|
1825
1863
|
) }),
|
|
1826
|
-
!error && totalPages > 1 && /* @__PURE__ */
|
|
1864
|
+
!error && totalPages > 1 && /* @__PURE__ */ jsx9(UnifiedPagination, { currentPage: page, totalPages })
|
|
1827
1865
|
] });
|
|
1828
|
-
return /* @__PURE__ */
|
|
1866
|
+
return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", preControls: form, children: body });
|
|
1829
1867
|
}
|
|
1830
1868
|
export {
|
|
1831
1869
|
HelpCenterCard,
|
|
@@ -1837,6 +1875,7 @@ export {
|
|
|
1837
1875
|
TicketDetailDrawer,
|
|
1838
1876
|
TicketLinkedDeliveryCard,
|
|
1839
1877
|
TicketOpenForm,
|
|
1878
|
+
TicketReplyComposer,
|
|
1840
1879
|
TicketRow,
|
|
1841
1880
|
isOptimistic,
|
|
1842
1881
|
mapTicketActionError,
|