@flamingo-stack/openframe-frontend-core 0.0.314 → 0.0.315-snapshot.20260624135932
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-NH2RY6VM.js → chunk-2Y4DLBFO.js} +108 -97
- package/dist/{chunk-NH2RY6VM.js.map → chunk-2Y4DLBFO.js.map} +1 -1
- package/dist/{chunk-VCJOLKED.cjs → chunk-4MCMPYEM.cjs} +12 -12
- package/dist/{chunk-VCJOLKED.cjs.map → chunk-4MCMPYEM.cjs.map} +1 -1
- package/dist/{chunk-E2LC43T3.js → chunk-4NVA6W3J.js} +27 -22
- package/dist/chunk-4NVA6W3J.js.map +1 -0
- package/dist/chunk-4V3TCOFC.cjs +394 -0
- package/dist/chunk-4V3TCOFC.cjs.map +1 -0
- package/dist/{chunk-46UZAYUT.cjs → chunk-63A53WQN.cjs} +33 -33
- package/dist/{chunk-46UZAYUT.cjs.map → chunk-63A53WQN.cjs.map} +1 -1
- package/dist/{chunk-L7BROXZ7.js → chunk-64DZ2J7Q.js} +5 -5
- package/dist/{chunk-OD3BEWDQ.js → chunk-6KERXOFE.js} +3 -3
- package/dist/{chunk-JALO4TAZ.js → chunk-AI5X5JTD.js} +4 -4
- package/dist/chunk-CSLMCBZV.js +1464 -0
- package/dist/chunk-CSLMCBZV.js.map +1 -0
- package/dist/{chunk-2LFQJYLQ.cjs → chunk-CUNMBP3A.cjs} +13 -13
- package/dist/{chunk-2LFQJYLQ.cjs.map → chunk-CUNMBP3A.cjs.map} +1 -1
- package/dist/{chunk-DD35H7HA.cjs → chunk-DHVL36CA.cjs} +40 -40
- package/dist/{chunk-DD35H7HA.cjs.map → chunk-DHVL36CA.cjs.map} +1 -1
- package/dist/chunk-FCEVVNWY.cjs +1916 -0
- package/dist/chunk-FCEVVNWY.cjs.map +1 -0
- package/dist/chunk-FOVX3W3C.cjs +1464 -0
- package/dist/chunk-FOVX3W3C.cjs.map +1 -0
- package/dist/{chunk-BJ6JXN5Z.js → chunk-GHVVOST5.js} +95 -116
- package/dist/chunk-GHVVOST5.js.map +1 -0
- package/dist/{chunk-TRSDXD23.js → chunk-JAZM3A7E.js} +2 -2
- package/dist/{chunk-XKVSR3IV.js → chunk-JEBL5PQK.js} +21 -35
- package/dist/{chunk-XKVSR3IV.js.map → chunk-JEBL5PQK.js.map} +1 -1
- package/dist/{chunk-5ATH263N.cjs → chunk-L5JSGNT3.cjs} +35 -35
- package/dist/{chunk-5ATH263N.cjs.map → chunk-L5JSGNT3.cjs.map} +1 -1
- package/dist/{chunk-WJCOWYAP.cjs → chunk-LAMDFGE3.cjs} +41 -36
- package/dist/chunk-LAMDFGE3.cjs.map +1 -0
- package/dist/{chunk-V4IIBNTA.js → chunk-LQHMXPOJ.js} +5 -5
- package/dist/{chunk-LGLPNWS6.cjs → chunk-LWNPMLIH.cjs} +3 -3
- package/dist/{chunk-LGLPNWS6.cjs.map → chunk-LWNPMLIH.cjs.map} +1 -1
- package/dist/chunk-M3NULYCR.js +1916 -0
- package/dist/chunk-M3NULYCR.js.map +1 -0
- package/dist/{chunk-UO27TVAO.js → chunk-OKGZK6TT.js} +3 -3
- package/dist/{chunk-BHOGI57O.cjs → chunk-OLEW7FYZ.cjs} +123 -144
- package/dist/chunk-OLEW7FYZ.cjs.map +1 -0
- package/dist/chunk-PIJ4JLJU.js +394 -0
- package/dist/chunk-PIJ4JLJU.js.map +1 -0
- package/dist/{chunk-E4CQ4RUG.js → chunk-Q4AMYLKX.js} +11 -11
- package/dist/{chunk-FQOTC3UU.cjs → chunk-QJGRP2YE.cjs} +4 -4
- package/dist/{chunk-FQOTC3UU.cjs.map → chunk-QJGRP2YE.cjs.map} +1 -1
- package/dist/{chunk-ZPK5HW7B.cjs → chunk-UGDGUO26.cjs} +3 -3
- package/dist/{chunk-ZPK5HW7B.cjs.map → chunk-UGDGUO26.cjs.map} +1 -1
- package/dist/{chunk-AD7TII2A.cjs → chunk-VCE3ZEN3.cjs} +5 -5
- package/dist/{chunk-AD7TII2A.cjs.map → chunk-VCE3ZEN3.cjs.map} +1 -1
- package/dist/{chunk-EC4DGRN6.cjs → chunk-XAQJ4ZLY.cjs} +459 -448
- package/dist/chunk-XAQJ4ZLY.cjs.map +1 -0
- package/dist/{chunk-ZW3NHMG7.js → chunk-YFGDZFUG.js} +4 -4
- package/dist/{chunk-JWX6NIQ4.js → chunk-Z3YORGG4.js} +2 -2
- package/dist/{chunk-UNKIRZVY.cjs → chunk-ZYGVJXJ5.cjs} +33 -47
- package/dist/chunk-ZYGVJXJ5.cjs.map +1 -0
- package/dist/components/case-studies/index.cjs +18 -18
- package/dist/components/case-studies/index.cjs.map +1 -1
- package/dist/components/case-studies/index.js +8 -8
- package/dist/components/chat/index.cjs +8 -8
- package/dist/components/chat/index.js +7 -7
- package/dist/components/contact/index.cjs +9 -9
- package/dist/components/contact/index.js +8 -8
- package/dist/components/docs/doc-viewer.d.ts +4 -0
- package/dist/components/docs/doc-viewer.d.ts.map +1 -1
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/docs/index.js +10 -10
- package/dist/components/embeds/index.cjs +9 -9
- package/dist/components/embeds/index.js +8 -8
- package/dist/components/faq/faq-document-page.d.ts +18 -20
- package/dist/components/faq/faq-document-page.d.ts.map +1 -1
- package/dist/components/faq/index.cjs +10 -10
- package/dist/components/faq/index.js +9 -9
- package/dist/components/features/index.cjs +8 -8
- package/dist/components/features/index.js +7 -7
- package/dist/components/features/push-button-selector.d.ts +1 -0
- package/dist/components/features/push-button-selector.d.ts.map +1 -1
- package/dist/components/help-center-pages/delivery-page.d.ts +27 -0
- package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.cjs +164 -0
- package/dist/components/help-center-pages/index.cjs.map +1 -0
- package/dist/components/help-center-pages/index.d.ts +25 -0
- package/dist/components/help-center-pages/index.d.ts.map +1 -0
- package/dist/components/help-center-pages/index.js +164 -0
- package/dist/components/help-center-pages/index.js.map +1 -0
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +41 -0
- package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/product-releases-list-page.d.ts +34 -0
- package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -0
- package/dist/components/help-center-pages/roadmap-page.d.ts +40 -0
- package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -0
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +177 -1555
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +348 -1726
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +4 -1
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +5 -1
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +8 -8
- package/dist/components/navigation/index.js +7 -7
- package/dist/components/onboarding-guides/index.cjs +15 -364
- package/dist/components/onboarding-guides/index.cjs.map +1 -1
- package/dist/components/onboarding-guides/index.js +20 -369
- package/dist/components/onboarding-guides/index.js.map +1 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts +9 -1
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +10 -10
- package/dist/components/related-content/index.js +9 -9
- package/dist/components/shared/dev-section/dev-section-page.d.ts +7 -1
- package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts +7 -1
- package/dist/components/shared/dev-section/dev-section-view.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts +5 -1
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/product-release/release-detail-page.d.ts +11 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/help-center-list.d.ts +5 -1
- package/dist/components/tickets/help-center-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +15 -1882
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +28 -1895
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +53 -53
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +4 -4
- package/dist/components/ui/index.cjs +8 -8
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +7 -7
- package/dist/hooks/index.cjs +5 -5
- package/dist/hooks/index.js +4 -4
- package/dist/index.cjs +10 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +9 -9
- package/package.json +7 -1
- package/src/components/docs/doc-viewer.tsx +21 -34
- package/src/components/faq/faq-document-page.tsx +33 -60
- package/src/components/features/push-button-selector.tsx +21 -3
- package/src/components/help-center-pages/delivery-page.tsx +45 -0
- package/src/components/help-center-pages/index.ts +41 -0
- package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +66 -0
- package/src/components/help-center-pages/product-releases-list-page.tsx +58 -0
- package/src/components/help-center-pages/roadmap-page.tsx +68 -0
- package/src/components/layout/page-layout.tsx +11 -0
- package/src/components/layout/title-block.tsx +15 -2
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +30 -19
- package/src/components/shared/dev-section/dev-section-page.tsx +29 -19
- package/src/components/shared/dev-section/dev-section-view.tsx +26 -19
- package/src/components/shared/legal-document/legal-document-page.tsx +19 -23
- package/src/components/shared/product-release/release-detail-page.tsx +36 -36
- package/src/components/tickets/help-center-list.tsx +11 -3
- package/dist/chunk-BHOGI57O.cjs.map +0 -1
- package/dist/chunk-BJ6JXN5Z.js.map +0 -1
- package/dist/chunk-E2LC43T3.js.map +0 -1
- package/dist/chunk-EC4DGRN6.cjs.map +0 -1
- package/dist/chunk-UNKIRZVY.cjs.map +0 -1
- package/dist/chunk-WJCOWYAP.cjs.map +0 -1
- /package/dist/{chunk-L7BROXZ7.js.map → chunk-64DZ2J7Q.js.map} +0 -0
- /package/dist/{chunk-OD3BEWDQ.js.map → chunk-6KERXOFE.js.map} +0 -0
- /package/dist/{chunk-JALO4TAZ.js.map → chunk-AI5X5JTD.js.map} +0 -0
- /package/dist/{chunk-TRSDXD23.js.map → chunk-JAZM3A7E.js.map} +0 -0
- /package/dist/{chunk-V4IIBNTA.js.map → chunk-LQHMXPOJ.js.map} +0 -0
- /package/dist/{chunk-UO27TVAO.js.map → chunk-OKGZK6TT.js.map} +0 -0
- /package/dist/{chunk-E4CQ4RUG.js.map → chunk-Q4AMYLKX.js.map} +0 -0
- /package/dist/{chunk-ZW3NHMG7.js.map → chunk-YFGDZFUG.js.map} +0 -0
- /package/dist/{chunk-JWX6NIQ4.js.map → chunk-Z3YORGG4.js.map} +0 -0
|
@@ -1,1907 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
HelpCenterCard,
|
|
4
|
+
HelpCenterCreateForm,
|
|
5
|
+
HelpCenterCreateFormSkeleton,
|
|
6
|
+
HelpCenterList,
|
|
7
|
+
TOAST_COPY,
|
|
8
|
+
TicketCenter,
|
|
9
|
+
TicketDetailDrawer,
|
|
10
|
+
TicketLinkedDeliveryCard,
|
|
11
|
+
TicketOpenForm,
|
|
12
|
+
TicketReplyComposer,
|
|
13
|
+
TicketRow,
|
|
14
|
+
isOptimistic,
|
|
15
|
+
mapTicketActionError,
|
|
16
|
+
useTicketActions,
|
|
17
|
+
useTicketEngagements,
|
|
18
|
+
useTicketsList
|
|
19
|
+
} from "../../chunk-M3NULYCR.js";
|
|
20
|
+
import "../../chunk-YFGDZFUG.js";
|
|
21
|
+
import "../../chunk-4NVA6W3J.js";
|
|
11
22
|
import "../../chunk-4XLJWX2N.js";
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
AlertDialog,
|
|
17
|
-
AlertDialogAction,
|
|
18
|
-
AlertDialogCancel,
|
|
19
|
-
AlertDialogContent,
|
|
20
|
-
AlertDialogDescription,
|
|
21
|
-
AlertDialogFooter,
|
|
22
|
-
AlertDialogHeader,
|
|
23
|
-
AlertDialogTitle,
|
|
24
|
-
Card,
|
|
25
|
-
ChatAttachmentAddButton,
|
|
26
|
-
ChatAttachmentChipStrip,
|
|
27
|
-
ChatInput,
|
|
28
|
-
ChatMessageRow,
|
|
29
|
-
ChatMessageRowSkeleton,
|
|
30
|
-
ChatTicketItem,
|
|
31
|
-
Label,
|
|
32
|
-
SquareAvatar,
|
|
33
|
-
StatusBadge,
|
|
34
|
-
Textarea,
|
|
35
|
-
TicketAttachmentsList,
|
|
36
|
-
devSectionAnchorId,
|
|
37
|
-
formatRelativeTime,
|
|
38
|
-
getStatusColorScheme,
|
|
39
|
-
useChatAttachments,
|
|
40
|
-
useChatIdentity
|
|
41
|
-
} from "../../chunk-NH2RY6VM.js";
|
|
42
|
-
import {
|
|
43
|
-
STICKY_HEADER_OFFSET_PX,
|
|
44
|
-
embedAuthedFetch,
|
|
45
|
-
scrollElementIntoView,
|
|
46
|
-
toast,
|
|
47
|
-
useScrollToHash
|
|
48
|
-
} from "../../chunk-E4CQ4RUG.js";
|
|
49
|
-
import {
|
|
50
|
-
useRequiredChatRuntime
|
|
51
|
-
} from "../../chunk-2FI3USTC.js";
|
|
23
|
+
import "../../chunk-2Y4DLBFO.js";
|
|
24
|
+
import "../../chunk-Q4AMYLKX.js";
|
|
25
|
+
import "../../chunk-J7AV6H63.js";
|
|
26
|
+
import "../../chunk-2FI3USTC.js";
|
|
52
27
|
import "../../chunk-EL5YVPD5.js";
|
|
53
|
-
import
|
|
54
|
-
usePathname,
|
|
55
|
-
useRouter,
|
|
56
|
-
useSearchParams
|
|
57
|
-
} from "../../chunk-PLJLE4A4.js";
|
|
28
|
+
import "../../chunk-PLJLE4A4.js";
|
|
58
29
|
import "../../chunk-LXC6P2EO.js";
|
|
59
|
-
import
|
|
60
|
-
Button,
|
|
61
|
-
Input,
|
|
62
|
-
Skeleton,
|
|
63
|
-
init_button2 as init_button
|
|
64
|
-
} from "../../chunk-JALO4TAZ.js";
|
|
65
|
-
import "../../chunk-OHPI2HRK.js";
|
|
66
|
-
import "../../chunk-V4IIBNTA.js";
|
|
67
|
-
import "../../chunk-XTCBRQN2.js";
|
|
68
|
-
import "../../chunk-J7AV6H63.js";
|
|
30
|
+
import "../../chunk-AI5X5JTD.js";
|
|
69
31
|
import "../../chunk-6U3IUD57.js";
|
|
32
|
+
import "../../chunk-OHPI2HRK.js";
|
|
33
|
+
import "../../chunk-LQHMXPOJ.js";
|
|
70
34
|
import "../../chunk-W72U7OU7.js";
|
|
35
|
+
import "../../chunk-XTCBRQN2.js";
|
|
71
36
|
import "../../chunk-PHWQLKVE.js";
|
|
72
37
|
import "../../chunk-GGWZFCYS.js";
|
|
73
|
-
|
|
74
|
-
// src/components/tickets/ticket-center.tsx
|
|
75
|
-
import { useCallback as useCallback4, useState as useState4 } from "react";
|
|
76
|
-
import { useQueryClient as useQueryClient2 } from "@tanstack/react-query";
|
|
77
|
-
init_button();
|
|
78
|
-
import { RefreshCw } from "lucide-react";
|
|
79
|
-
|
|
80
|
-
// src/components/tickets/ticket-open-form.tsx
|
|
81
|
-
import { useState } from "react";
|
|
82
|
-
init_button();
|
|
83
|
-
|
|
84
|
-
// src/components/tickets/types.ts
|
|
85
|
-
function isOptimistic(t) {
|
|
86
|
-
return t._optimistic === true;
|
|
87
|
-
}
|
|
88
|
-
var TICKET_TEXT_MAX_CHARS = 5e3;
|
|
89
|
-
var TICKET_LIVE_POLL_MS = 8e3;
|
|
90
|
-
var TOAST_COPY = {
|
|
91
|
-
open_success: { title: "Ticket opened", description: "We received your message and will follow up shortly." },
|
|
92
|
-
open_mirror_pending: { title: "Ticket opened", description: "Syncing \u2014 your ticket will appear momentarily." },
|
|
93
|
-
close_success: { title: "Ticket closed" },
|
|
94
|
-
reopen_success: { title: "Ticket reopened" },
|
|
95
|
-
comment_success: { title: "Comment added" },
|
|
96
|
-
attach_success: { title: "Files attached" }
|
|
97
|
-
// Failure variants are constructed dynamically from MappedTicketActionError.
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// src/components/tickets/ticket-open-form.tsx
|
|
101
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
102
|
-
var COUNTER_VISIBLE_AT = Math.floor(TICKET_TEXT_MAX_CHARS * 0.8);
|
|
103
|
-
function TicketOpenForm({
|
|
104
|
-
onSubmit,
|
|
105
|
-
isSubmitting,
|
|
106
|
-
supportSystemDown
|
|
107
|
-
}) {
|
|
108
|
-
const [subject, setSubject] = useState("");
|
|
109
|
-
const [content, setContent] = useState("");
|
|
110
|
-
const { attachments, readyAttachments, hasInflightUploads, addFiles, removeAttachment, clear } = useChatAttachments();
|
|
111
|
-
const trimmedSubject = subject.trim();
|
|
112
|
-
const trimmedContent = content.trim();
|
|
113
|
-
const overCap = content.length > TICKET_TEXT_MAX_CHARS;
|
|
114
|
-
const showCounter = content.length >= COUNTER_VISIBLE_AT;
|
|
115
|
-
const canSubmit = !isSubmitting && !supportSystemDown && !hasInflightUploads && trimmedSubject.length > 0 && trimmedContent.length > 0 && !overCap;
|
|
116
|
-
const handleSubmit = async (e) => {
|
|
117
|
-
e.preventDefault();
|
|
118
|
-
if (!canSubmit) return;
|
|
119
|
-
const ok = await onSubmit({
|
|
120
|
-
subject: trimmedSubject,
|
|
121
|
-
content: trimmedContent,
|
|
122
|
-
attachments: readyAttachments
|
|
123
|
-
});
|
|
124
|
-
if (ok) {
|
|
125
|
-
setSubject("");
|
|
126
|
-
setContent("");
|
|
127
|
-
clear();
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
return /* @__PURE__ */ jsx(Card, { className: "p-6", children: /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col md:flex-row gap-6", children: [
|
|
131
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 md:max-w-md", children: [
|
|
132
|
-
/* @__PURE__ */ jsx("h2", { className: "text-2xl font-semibold text-ods-text-primary mb-2", children: "Need Support?" }),
|
|
133
|
-
/* @__PURE__ */ jsx("p", { className: "text-ods-text-secondary text-sm", children: "Can't find what you're looking for? Submit a support ticket below \u2014 we'll follow up shortly." }),
|
|
134
|
-
supportSystemDown && /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-ods-error", children: "Support system temporarily unavailable. Please try again shortly." })
|
|
135
|
-
] }),
|
|
136
|
-
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col gap-4", children: [
|
|
137
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
138
|
-
/* @__PURE__ */ jsx(
|
|
139
|
-
"label",
|
|
140
|
-
{
|
|
141
|
-
htmlFor: "ticket-subject",
|
|
142
|
-
className: "block text-sm font-medium text-ods-text-primary mb-1",
|
|
143
|
-
children: "Ticket Subject"
|
|
144
|
-
}
|
|
145
|
-
),
|
|
146
|
-
/* @__PURE__ */ jsx(
|
|
147
|
-
Input,
|
|
148
|
-
{
|
|
149
|
-
id: "ticket-subject",
|
|
150
|
-
type: "text",
|
|
151
|
-
placeholder: "Enter Subject Here",
|
|
152
|
-
value: subject,
|
|
153
|
-
onChange: (e) => setSubject(e.target.value),
|
|
154
|
-
disabled: isSubmitting || supportSystemDown,
|
|
155
|
-
maxLength: 200
|
|
156
|
-
}
|
|
157
|
-
)
|
|
158
|
-
] }),
|
|
159
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
160
|
-
/* @__PURE__ */ jsx(
|
|
161
|
-
"label",
|
|
162
|
-
{
|
|
163
|
-
htmlFor: "ticket-content",
|
|
164
|
-
className: "block text-sm font-medium text-ods-text-primary mb-1",
|
|
165
|
-
children: "Your Message"
|
|
166
|
-
}
|
|
167
|
-
),
|
|
168
|
-
/* @__PURE__ */ jsx(
|
|
169
|
-
Textarea,
|
|
170
|
-
{
|
|
171
|
-
id: "ticket-content",
|
|
172
|
-
placeholder: "Describe your issue or question in detail...",
|
|
173
|
-
value: content,
|
|
174
|
-
onChange: (e) => setContent(e.target.value),
|
|
175
|
-
disabled: isSubmitting || supportSystemDown,
|
|
176
|
-
rows: 5,
|
|
177
|
-
className: "resize-none"
|
|
178
|
-
}
|
|
179
|
-
),
|
|
180
|
-
showCounter && /* @__PURE__ */ jsxs(
|
|
181
|
-
"p",
|
|
182
|
-
{
|
|
183
|
-
className: `mt-1 text-xs text-right ${overCap ? "text-ods-error" : "text-ods-text-secondary"}`,
|
|
184
|
-
children: [
|
|
185
|
-
content.length,
|
|
186
|
-
"/",
|
|
187
|
-
TICKET_TEXT_MAX_CHARS
|
|
188
|
-
]
|
|
189
|
-
}
|
|
190
|
-
)
|
|
191
|
-
] }),
|
|
192
|
-
/* @__PURE__ */ jsx(
|
|
193
|
-
ChatAttachmentChipStrip,
|
|
194
|
-
{
|
|
195
|
-
attachments,
|
|
196
|
-
onRemove: removeAttachment,
|
|
197
|
-
disabled: isSubmitting || supportSystemDown
|
|
198
|
-
}
|
|
199
|
-
),
|
|
200
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
201
|
-
/* @__PURE__ */ jsx(
|
|
202
|
-
ChatAttachmentAddButton,
|
|
203
|
-
{
|
|
204
|
-
attachmentsEnabled: !supportSystemDown,
|
|
205
|
-
attachmentsCount: attachments.length,
|
|
206
|
-
onAddFiles: addFiles,
|
|
207
|
-
disabled: isSubmitting
|
|
208
|
-
}
|
|
209
|
-
),
|
|
210
|
-
/* @__PURE__ */ jsx(
|
|
211
|
-
Button,
|
|
212
|
-
{
|
|
213
|
-
type: "submit",
|
|
214
|
-
disabled: !canSubmit,
|
|
215
|
-
loading: isSubmitting,
|
|
216
|
-
children: "Open Ticket"
|
|
217
|
-
}
|
|
218
|
-
)
|
|
219
|
-
] })
|
|
220
|
-
] })
|
|
221
|
-
] }) });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// src/components/tickets/ticket-row.tsx
|
|
225
|
-
import { useCallback as useCallback2, useRef } from "react";
|
|
226
|
-
|
|
227
|
-
// src/components/collapsible.tsx
|
|
228
|
-
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
229
|
-
var Collapsible = CollapsiblePrimitive.Root;
|
|
230
|
-
var CollapsibleContent2 = CollapsiblePrimitive.CollapsibleContent;
|
|
231
|
-
|
|
232
|
-
// src/components/tickets/ticket-detail-drawer.tsx
|
|
233
|
-
init_button();
|
|
234
|
-
import { useStickToBottom } from "use-stick-to-bottom";
|
|
235
|
-
|
|
236
|
-
// src/components/tickets/hooks/use-ticket-engagements.ts
|
|
237
|
-
import { useQuery } from "@tanstack/react-query";
|
|
238
|
-
var LIST_ENGAGEMENTS_ENDPOINT = "/api/chat/agent/list-engagements";
|
|
239
|
-
function useTicketEngagements(externalTicketId, enabled = true, refetchInterval = false) {
|
|
240
|
-
const identity = useChatIdentity();
|
|
241
|
-
const identityKey = identity.user?.email ?? "anon";
|
|
242
|
-
const listEngagementsEndpoint = useRequiredChatRuntime().endpoints.listEngagementsUrl ?? LIST_ENGAGEMENTS_ENDPOINT;
|
|
243
|
-
const fetchable = enabled && !!externalTicketId && !externalTicketId.startsWith("temp-");
|
|
244
|
-
const queryEnabled = fetchable && identity.authTier !== "anon" && !!identity.user?.email;
|
|
245
|
-
const query = useQuery({
|
|
246
|
-
queryKey: ["ticket-engagements", externalTicketId, identityKey],
|
|
247
|
-
enabled: queryEnabled,
|
|
248
|
-
// Caches OFF — same reasoning as `useTicketsList`. The conversation
|
|
249
|
-
// timeline must reflect HubSpot truth on every drawer-open; a stale
|
|
250
|
-
// window risks hiding a freshly-arrived agent reply.
|
|
251
|
-
staleTime: 0,
|
|
252
|
-
gcTime: 0,
|
|
253
|
-
refetchOnMount: "always",
|
|
254
|
-
refetchOnWindowFocus: true,
|
|
255
|
-
// Live conversation: poll while the caller opts in (drawer open). New
|
|
256
|
-
// agent replies + attachments appear within one interval without a
|
|
257
|
-
// manual refresh. `refetchIntervalInBackground` stays false (default)
|
|
258
|
-
// so polling pauses on a hidden tab.
|
|
259
|
-
refetchInterval,
|
|
260
|
-
queryFn: async () => {
|
|
261
|
-
const response = await embedAuthedFetch(listEngagementsEndpoint, {
|
|
262
|
-
method: "POST",
|
|
263
|
-
body: JSON.stringify({ ticket_id: externalTicketId })
|
|
264
|
-
});
|
|
265
|
-
if (!response.ok) {
|
|
266
|
-
const text = await response.text().catch(() => "");
|
|
267
|
-
throw new Error(`list-engagements failed: ${response.status} ${text.slice(0, 200)}`);
|
|
268
|
-
}
|
|
269
|
-
const body = await response.json();
|
|
270
|
-
return Array.isArray(body.engagements) ? body.engagements : [];
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
return {
|
|
274
|
-
engagements: query.data ?? [],
|
|
275
|
-
// Loading-state truth that prevents the "body → blink → skeleton → data"
|
|
276
|
-
// double-flash. The bug: `useChatIdentity` starts at anon defaults and
|
|
277
|
-
// resolves async, so on the first render `queryEnabled` is false and the
|
|
278
|
-
// OLD `queryEnabled && query.isLoading` returned FALSE — the panel rendered
|
|
279
|
-
// the ticket body, THEN identity resolved, the query enabled, isLoading
|
|
280
|
-
// flipped true → skeleton appeared (the blink), then data landed.
|
|
281
|
-
//
|
|
282
|
-
// Fix: for a fetchable ticket we are "loading" whenever we don't yet have
|
|
283
|
-
// the timeline to show — that includes the window while identity is still
|
|
284
|
-
// resolving (so we skeleton from the FIRST render, never the body) AND the
|
|
285
|
-
// cold query fetch (`data === undefined`). A background poll keeps
|
|
286
|
-
// `query.data` defined, so it never re-flashes the skeleton. Non-fetchable
|
|
287
|
-
// (optimistic/disabled) or a resolved-anon viewer → not loading.
|
|
288
|
-
isLoading: fetchable && (identity.isLoading || queryEnabled && query.data === void 0),
|
|
289
|
-
isFetching: query.isFetching,
|
|
290
|
-
error: query.error ?? null,
|
|
291
|
-
refetch: () => {
|
|
292
|
-
void query.refetch();
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// src/components/tickets/ticket-linked-delivery-card.tsx
|
|
298
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
299
|
-
function TicketLinkedDeliveryCard({
|
|
300
|
-
clickup,
|
|
301
|
-
className
|
|
302
|
-
}) {
|
|
303
|
-
const item = {
|
|
304
|
-
id: clickup.external_id,
|
|
305
|
-
title: clickup.title ?? "Linked delivery task",
|
|
306
|
-
description: clickup.description ?? "",
|
|
307
|
-
status: clickup.status ?? "unknown",
|
|
308
|
-
statusColor: clickup.status_color ?? "#87909e",
|
|
309
|
-
taskType: clickup.task_type ?? "Request",
|
|
310
|
-
customItemId: clickup.custom_item_id,
|
|
311
|
-
listNames: clickup.list_names,
|
|
312
|
-
dateOpened: clickup.date_opened ?? 0,
|
|
313
|
-
dateUpdated: clickup.date_updated ?? clickup.date_opened ?? Date.now(),
|
|
314
|
-
dateClosed: clickup.date_closed,
|
|
315
|
-
clickupUrl: clickup.clickup_url ?? ""
|
|
316
|
-
};
|
|
317
|
-
return /* @__PURE__ */ jsx2(
|
|
318
|
-
"div",
|
|
319
|
-
{
|
|
320
|
-
className: `rounded-md border border-ods-border bg-ods-bg overflow-hidden ${className ?? ""}`,
|
|
321
|
-
children: /* @__PURE__ */ jsx2(
|
|
322
|
-
DeliveryRow,
|
|
323
|
-
{
|
|
324
|
-
item,
|
|
325
|
-
href: clickup.delivery_href,
|
|
326
|
-
caption: "Linked delivery"
|
|
327
|
-
}
|
|
328
|
-
)
|
|
329
|
-
}
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// src/components/tickets/ticket-reply-composer.tsx
|
|
334
|
-
init_button();
|
|
335
|
-
import { useCallback, useState as useState2 } from "react";
|
|
336
|
-
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
337
|
-
function TicketReplyComposer({
|
|
338
|
-
ticket,
|
|
339
|
-
busy,
|
|
340
|
-
supportSystemDown,
|
|
341
|
-
onSendMessage,
|
|
342
|
-
onClose
|
|
343
|
-
}) {
|
|
344
|
-
const [resolution, setResolution] = useState2("");
|
|
345
|
-
const [closeDialogOpen, setCloseDialogOpen] = useState2(false);
|
|
346
|
-
const attachments = useChatAttachments();
|
|
347
|
-
const ticketRef = { id: ticket.id, external_id: ticket.external_id };
|
|
348
|
-
const hasReadyFiles = attachments.readyAttachments.length > 0;
|
|
349
|
-
const handleSend = useCallback(
|
|
350
|
-
async (text) => {
|
|
351
|
-
const ref = { id: ticket.id, external_id: ticket.external_id };
|
|
352
|
-
const ok = await onSendMessage(ref, text.trim(), attachments.readyAttachments);
|
|
353
|
-
if (ok) attachments.clear();
|
|
354
|
-
return ok;
|
|
355
|
-
},
|
|
356
|
-
// Depend on the reactive projections, not the whole bag (a fresh object each
|
|
357
|
-
// render). `readyAttachments` is memo-stable; `clear` is callback-stable.
|
|
358
|
-
[
|
|
359
|
-
onSendMessage,
|
|
360
|
-
ticket.id,
|
|
361
|
-
ticket.external_id,
|
|
362
|
-
attachments.readyAttachments,
|
|
363
|
-
attachments.clear
|
|
364
|
-
]
|
|
365
|
-
);
|
|
366
|
-
const confirmClose = async () => {
|
|
367
|
-
setCloseDialogOpen(false);
|
|
368
|
-
await onClose(ticketRef, resolution.trim() || void 0);
|
|
369
|
-
setResolution("");
|
|
370
|
-
};
|
|
371
|
-
const disabled = busy || supportSystemDown;
|
|
372
|
-
return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
|
|
373
|
-
/* @__PURE__ */ jsx3(
|
|
374
|
-
ChatAttachmentChipStrip,
|
|
375
|
-
{
|
|
376
|
-
attachments: attachments.attachments,
|
|
377
|
-
onRemove: attachments.removeAttachment,
|
|
378
|
-
disabled,
|
|
379
|
-
size: "compact"
|
|
380
|
-
}
|
|
381
|
-
),
|
|
382
|
-
/* @__PURE__ */ jsx3(
|
|
383
|
-
ChatInput,
|
|
384
|
-
{
|
|
385
|
-
fullWidth: true,
|
|
386
|
-
autoFocus: true,
|
|
387
|
-
placeholder: "Type a reply\u2026",
|
|
388
|
-
sending: busy || attachments.hasInflightUploads,
|
|
389
|
-
disabled: supportSystemDown,
|
|
390
|
-
allowEmptySend: hasReadyFiles,
|
|
391
|
-
maxLength: TICKET_TEXT_MAX_CHARS,
|
|
392
|
-
onSend: handleSend
|
|
393
|
-
}
|
|
394
|
-
),
|
|
395
|
-
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", children: [
|
|
396
|
-
!supportSystemDown && /* @__PURE__ */ jsx3(
|
|
397
|
-
ChatAttachmentAddButton,
|
|
398
|
-
{
|
|
399
|
-
attachmentsEnabled: true,
|
|
400
|
-
attachmentsCount: attachments.attachments.length,
|
|
401
|
-
onAddFiles: attachments.addFiles,
|
|
402
|
-
disabled,
|
|
403
|
-
size: "compact"
|
|
404
|
-
}
|
|
405
|
-
),
|
|
406
|
-
/* @__PURE__ */ jsx3("div", { className: "flex-1 min-w-0" }),
|
|
407
|
-
/* @__PURE__ */ jsx3(
|
|
408
|
-
Button,
|
|
409
|
-
{
|
|
410
|
-
type: "button",
|
|
411
|
-
variant: "transparent",
|
|
412
|
-
size: "small",
|
|
413
|
-
onClick: () => setCloseDialogOpen(true),
|
|
414
|
-
disabled,
|
|
415
|
-
className: "text-ods-text-secondary hover:text-ods-text-primary",
|
|
416
|
-
children: "Close ticket"
|
|
417
|
-
}
|
|
418
|
-
)
|
|
419
|
-
] }),
|
|
420
|
-
/* @__PURE__ */ jsx3(AlertDialog, { open: closeDialogOpen, onOpenChange: setCloseDialogOpen, children: /* @__PURE__ */ jsxs2(AlertDialogContent, { className: "bg-ods-card border-ods-border", children: [
|
|
421
|
-
/* @__PURE__ */ jsxs2(AlertDialogHeader, { children: [
|
|
422
|
-
/* @__PURE__ */ jsx3(AlertDialogTitle, { className: "text-ods-text-primary", children: "Close this ticket?" }),
|
|
423
|
-
/* @__PURE__ */ jsx3(AlertDialogDescription, { className: "text-ods-text-secondary", children: "Add an optional resolution note below. You can reopen the ticket later if needed." })
|
|
424
|
-
] }),
|
|
425
|
-
/* @__PURE__ */ jsx3(
|
|
426
|
-
Textarea,
|
|
427
|
-
{
|
|
428
|
-
value: resolution,
|
|
429
|
-
onChange: (e) => setResolution(e.target.value),
|
|
430
|
-
placeholder: "Resolution (optional)",
|
|
431
|
-
rows: 3,
|
|
432
|
-
maxLength: TICKET_TEXT_MAX_CHARS,
|
|
433
|
-
className: "mt-2"
|
|
434
|
-
}
|
|
435
|
-
),
|
|
436
|
-
/* @__PURE__ */ jsxs2(AlertDialogFooter, { children: [
|
|
437
|
-
/* @__PURE__ */ jsx3(
|
|
438
|
-
AlertDialogCancel,
|
|
439
|
-
{
|
|
440
|
-
disabled: busy,
|
|
441
|
-
className: "bg-transparent border-ods-border text-ods-text-primary hover:bg-ods-border",
|
|
442
|
-
children: "Cancel"
|
|
443
|
-
}
|
|
444
|
-
),
|
|
445
|
-
/* @__PURE__ */ jsx3(
|
|
446
|
-
AlertDialogAction,
|
|
447
|
-
{
|
|
448
|
-
onClick: () => void confirmClose(),
|
|
449
|
-
disabled: busy,
|
|
450
|
-
className: "bg-ods-accent text-ods-text-on-accent hover:bg-ods-accent-hover",
|
|
451
|
-
children: "Close ticket"
|
|
452
|
-
}
|
|
453
|
-
)
|
|
454
|
-
] })
|
|
455
|
-
] }) })
|
|
456
|
-
] });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// src/components/tickets/ticket-detail-drawer.tsx
|
|
460
|
-
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
461
|
-
function TicketDetailDrawer({
|
|
462
|
-
ticket,
|
|
463
|
-
busy,
|
|
464
|
-
supportSystemDown,
|
|
465
|
-
onSendMessage,
|
|
466
|
-
onClose,
|
|
467
|
-
onReopen,
|
|
468
|
-
onActionCollapsed,
|
|
469
|
-
replyError,
|
|
470
|
-
onClearReplyError
|
|
471
|
-
}) {
|
|
472
|
-
const isClosed = (ticket.status ?? "").toUpperCase() === "CLOSED";
|
|
473
|
-
return /* @__PURE__ */ jsxs3("div", { className: "bg-ods-card border-t border-ods-border px-4 py-4 flex flex-col gap-4", children: [
|
|
474
|
-
/* @__PURE__ */ jsx4(AssignedAgentRow, { assignedOwner: ticket.assignedOwner }),
|
|
475
|
-
ticket.clickup && /* @__PURE__ */ jsx4(TicketLinkedDeliveryCard, { clickup: ticket.clickup }),
|
|
476
|
-
/* @__PURE__ */ jsxs3("div", { children: [
|
|
477
|
-
/* @__PURE__ */ jsx4("p", { className: "text-xs font-medium text-ods-text-secondary mb-2 uppercase tracking-wider", children: "Conversation" }),
|
|
478
|
-
/* @__PURE__ */ jsx4(TicketTimelinePanel, { ticket })
|
|
479
|
-
] }),
|
|
480
|
-
/* @__PURE__ */ jsxs3("div", { className: "border-t border-ods-border pt-4", children: [
|
|
481
|
-
replyError && /* @__PURE__ */ jsx4(
|
|
482
|
-
ReplyFailureBanner,
|
|
483
|
-
{
|
|
484
|
-
error: replyError,
|
|
485
|
-
onDismiss: onClearReplyError ?? (() => void 0)
|
|
486
|
-
}
|
|
487
|
-
),
|
|
488
|
-
isClosed ? /* @__PURE__ */ jsx4(
|
|
489
|
-
ReopenAction,
|
|
490
|
-
{
|
|
491
|
-
ticketRef: { id: ticket.id, external_id: ticket.external_id },
|
|
492
|
-
busy,
|
|
493
|
-
supportSystemDown,
|
|
494
|
-
onReopen,
|
|
495
|
-
onActionCollapsed
|
|
496
|
-
}
|
|
497
|
-
) : /* @__PURE__ */ jsx4(
|
|
498
|
-
TicketReplyComposer,
|
|
499
|
-
{
|
|
500
|
-
ticket,
|
|
501
|
-
busy,
|
|
502
|
-
supportSystemDown,
|
|
503
|
-
onSendMessage,
|
|
504
|
-
onClose
|
|
505
|
-
}
|
|
506
|
-
)
|
|
507
|
-
] })
|
|
508
|
-
] });
|
|
509
|
-
}
|
|
510
|
-
var TURN_SEPARATOR_RE = /[\s]{1,16}---[\s]{1,16}/g;
|
|
511
|
-
var TICKET_FEED_FRAME = "bg-ods-card border border-ods-border rounded-[6px] overflow-y-auto w-full";
|
|
512
|
-
var TICKET_FEED_HEIGHT = "h-[60vh] md:h-[420px]";
|
|
513
|
-
var TICKET_FEED_INNER = "flex flex-col gap-4 md:gap-6 px-4 md:px-6 py-4 md:py-6";
|
|
514
|
-
var TICKET_FEED_SKELETON_ROWS = 6;
|
|
515
|
-
function TicketTimelinePanel({ ticket }) {
|
|
516
|
-
const identity = useChatIdentity();
|
|
517
|
-
const externalId = isOptimistic(ticket) ? null : ticket.external_id;
|
|
518
|
-
const { engagements, isLoading } = useTicketEngagements(
|
|
519
|
-
externalId,
|
|
520
|
-
!!externalId,
|
|
521
|
-
TICKET_LIVE_POLL_MS
|
|
522
|
-
);
|
|
523
|
-
const { scrollRef, contentRef } = useStickToBottom({ initial: "instant", resize: "smooth" });
|
|
524
|
-
const bodyTurns = ticket.body ? ticket.body.split(TURN_SEPARATOR_RE).map((t) => t.trim()).filter(Boolean) : [];
|
|
525
|
-
const customerEngagementBodies = new Set(
|
|
526
|
-
engagements.filter((e) => e.authorRole === "customer").map((e) => (e.body ?? "").trim()).filter(Boolean)
|
|
527
|
-
);
|
|
528
|
-
const suppressBodyTurnZero = bodyTurns.length > 0 && customerEngagementBodies.has(bodyTurns[0]);
|
|
529
|
-
const sessionEmailLower = identity.user?.email?.trim().toLowerCase() ?? null;
|
|
530
|
-
const isViewerTheCustomer = !!sessionEmailLower && ticket.customer_emails.some((e) => e.trim().toLowerCase() === sessionEmailLower);
|
|
531
|
-
const viewerName = identity.user?.name?.trim() || null;
|
|
532
|
-
const ticketCustomerName = ticket.customer_name?.trim() || null;
|
|
533
|
-
const customerName = (isViewerTheCustomer ? viewerName : null) || ticketCustomerName || viewerName || identity.user?.email || "You";
|
|
534
|
-
const customerAvatar = isViewerTheCustomer ? identity.user?.avatarUrl ?? void 0 : void 0;
|
|
535
|
-
if (isLoading) {
|
|
536
|
-
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)) }) });
|
|
537
|
-
}
|
|
538
|
-
if (bodyTurns.length === 0 && engagements.length === 0) {
|
|
539
|
-
return /* @__PURE__ */ jsx4(
|
|
540
|
-
EmptyState,
|
|
541
|
-
{
|
|
542
|
-
type: "generic",
|
|
543
|
-
title: "No conversation yet",
|
|
544
|
-
description: "Reply below to start the thread with the support team.",
|
|
545
|
-
showCTA: false
|
|
546
|
-
}
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
return /* @__PURE__ */ jsx4("div", { ref: scrollRef, className: `${TICKET_FEED_FRAME} ${TICKET_FEED_HEIGHT}`, children: /* @__PURE__ */ jsxs3("div", { ref: contentRef, className: TICKET_FEED_INNER, children: [
|
|
550
|
-
bodyTurns.map((turn, i) => {
|
|
551
|
-
if (i === 0 && suppressBodyTurnZero) return null;
|
|
552
|
-
const isResolution = turn.startsWith("[Resolution]");
|
|
553
|
-
const text = isResolution ? turn.replace(/^\[Resolution\]\s*/, "") : turn;
|
|
554
|
-
return /* @__PURE__ */ jsx4(
|
|
555
|
-
ChatMessageRow,
|
|
556
|
-
{
|
|
557
|
-
displayName: customerName,
|
|
558
|
-
avatarUrl: customerAvatar,
|
|
559
|
-
body: text
|
|
560
|
-
},
|
|
561
|
-
`body-${i}-${turn.slice(0, 24)}`
|
|
562
|
-
);
|
|
563
|
-
}),
|
|
564
|
-
engagements.map((eng) => {
|
|
565
|
-
const isCustomer = eng.authorRole === "customer";
|
|
566
|
-
const isOwnReply = isCustomer && !!eng.authorId && !!identity.user?.email && eng.authorId.toLowerCase() === identity.user.email.toLowerCase();
|
|
567
|
-
let author;
|
|
568
|
-
let avatarSrc;
|
|
569
|
-
if (isCustomer && isOwnReply) {
|
|
570
|
-
author = identity.user?.name?.trim() || customerName;
|
|
571
|
-
avatarSrc = identity.user?.avatarUrl ?? void 0;
|
|
572
|
-
} else if (isCustomer) {
|
|
573
|
-
author = ticketCustomerName || "Customer";
|
|
574
|
-
avatarSrc = void 0;
|
|
575
|
-
} else if (eng.authorName && eng.authorAvatarUrl) {
|
|
576
|
-
author = eng.authorName;
|
|
577
|
-
avatarSrc = eng.authorAvatarUrl;
|
|
578
|
-
} else {
|
|
579
|
-
author = "Support team";
|
|
580
|
-
avatarSrc = void 0;
|
|
581
|
-
}
|
|
582
|
-
const engAttachments = mapEngagementAttachments(eng.attachments);
|
|
583
|
-
return /* @__PURE__ */ jsx4(
|
|
584
|
-
ChatMessageRow,
|
|
585
|
-
{
|
|
586
|
-
displayName: author,
|
|
587
|
-
avatarUrl: avatarSrc,
|
|
588
|
-
timeLabel: eng.createdAt ? formatRelativeTime(eng.createdAt) : null,
|
|
589
|
-
body: stripAttachmentsPreamble(eng.body ?? ""),
|
|
590
|
-
footer: engAttachments.length > 0 ? /* @__PURE__ */ jsx4("div", { className: "mt-2", children: /* @__PURE__ */ jsx4(TicketAttachmentsList, { attachments: engAttachments, size: "compact" }) }) : null
|
|
591
|
-
},
|
|
592
|
-
eng.id
|
|
593
|
-
);
|
|
594
|
-
})
|
|
595
|
-
] }) });
|
|
596
|
-
}
|
|
597
|
-
function mapEngagementAttachments(files) {
|
|
598
|
-
return files.map((f) => ({
|
|
599
|
-
id: f.id,
|
|
600
|
-
fileName: f.name ?? `file-${f.id}`,
|
|
601
|
-
fileSize: f.size ? formatBytes(f.size) : "",
|
|
602
|
-
// Show an inline thumbnail for image attachments (the signed `url` is a
|
|
603
|
-
// viewable URL). Non-images fall back to the file-type icon. SquareAvatar
|
|
604
|
-
// degrades to initials on a broken/expired image URL.
|
|
605
|
-
thumbnailSrc: f.url && (f.mime?.startsWith("image/") ?? false) ? f.url : void 0,
|
|
606
|
-
onDownload: f.url ? () => window.open(f.url, "_blank", "noopener,noreferrer") : void 0
|
|
607
|
-
}));
|
|
608
|
-
}
|
|
609
|
-
function formatBytes(size) {
|
|
610
|
-
if (size < 1024) return `${size} B`;
|
|
611
|
-
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|
612
|
-
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
613
|
-
}
|
|
614
|
-
var ATTACHMENTS_PREAMBLE_RE = /\s*\n\s*Attachments\b[^]*$/i;
|
|
615
|
-
function stripAttachmentsPreamble(body) {
|
|
616
|
-
return body.replace(ATTACHMENTS_PREAMBLE_RE, "").trim();
|
|
617
|
-
}
|
|
618
|
-
function ReopenAction({
|
|
619
|
-
ticketRef,
|
|
620
|
-
busy,
|
|
621
|
-
supportSystemDown,
|
|
622
|
-
onReopen,
|
|
623
|
-
onActionCollapsed
|
|
624
|
-
}) {
|
|
625
|
-
const handleReopen = async () => {
|
|
626
|
-
void await onReopen(ticketRef);
|
|
627
|
-
};
|
|
628
|
-
return /* @__PURE__ */ jsx4("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx4(
|
|
629
|
-
Button,
|
|
630
|
-
{
|
|
631
|
-
type: "button",
|
|
632
|
-
variant: "outline",
|
|
633
|
-
size: "small",
|
|
634
|
-
onClick: () => void handleReopen(),
|
|
635
|
-
disabled: busy || supportSystemDown,
|
|
636
|
-
loading: busy,
|
|
637
|
-
children: "Reopen"
|
|
638
|
-
}
|
|
639
|
-
) });
|
|
640
|
-
}
|
|
641
|
-
function ReplyFailureBanner({
|
|
642
|
-
error,
|
|
643
|
-
onDismiss
|
|
644
|
-
}) {
|
|
645
|
-
return /* @__PURE__ */ jsxs3(
|
|
646
|
-
"div",
|
|
647
|
-
{
|
|
648
|
-
role: "status",
|
|
649
|
-
"aria-live": "polite",
|
|
650
|
-
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",
|
|
651
|
-
children: [
|
|
652
|
-
/* @__PURE__ */ jsx4("span", { className: "font-medium leading-snug", children: error.message }),
|
|
653
|
-
/* @__PURE__ */ jsx4(
|
|
654
|
-
Button,
|
|
655
|
-
{
|
|
656
|
-
type: "button",
|
|
657
|
-
variant: "transparent",
|
|
658
|
-
onClick: onDismiss,
|
|
659
|
-
"aria-label": "Dismiss reply failure",
|
|
660
|
-
className: "ml-auto px-2 py-0.5 text-xs font-medium uppercase tracking-wider text-ods-attention-red-error hover:bg-ods-attention-red-error/10 border-transparent",
|
|
661
|
-
children: "Dismiss"
|
|
662
|
-
}
|
|
663
|
-
)
|
|
664
|
-
]
|
|
665
|
-
}
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
function AssignedAgentRow({
|
|
669
|
-
assignedOwner
|
|
670
|
-
}) {
|
|
671
|
-
const trimmedName = assignedOwner?.name?.trim() || null;
|
|
672
|
-
const emailFallback = assignedOwner?.email?.trim() || null;
|
|
673
|
-
const displayLabel = trimmedName ?? (emailFallback ? emailFallback.split("@")[0] : null);
|
|
674
|
-
return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 text-xs", children: [
|
|
675
|
-
/* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary uppercase tracking-wider font-medium", children: "Assigned to" }),
|
|
676
|
-
displayLabel ? /* @__PURE__ */ jsxs3("span", { className: "flex items-center gap-1.5 text-ods-text-primary font-medium", children: [
|
|
677
|
-
/* @__PURE__ */ jsx4(
|
|
678
|
-
SquareAvatar,
|
|
679
|
-
{
|
|
680
|
-
size: "sm",
|
|
681
|
-
variant: "round",
|
|
682
|
-
src: assignedOwner?.avatarUrl ?? void 0,
|
|
683
|
-
alt: displayLabel,
|
|
684
|
-
fallback: displayLabel
|
|
685
|
-
}
|
|
686
|
-
),
|
|
687
|
-
displayLabel
|
|
688
|
-
] }) : /* @__PURE__ */ jsx4("span", { className: "text-ods-text-secondary italic", children: "Unassigned" })
|
|
689
|
-
] });
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// src/components/tickets/ticket-row.tsx
|
|
693
|
-
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
694
|
-
function TicketRow({
|
|
695
|
-
ticket,
|
|
696
|
-
expanded,
|
|
697
|
-
onToggle,
|
|
698
|
-
busy,
|
|
699
|
-
supportSystemDown,
|
|
700
|
-
onSendMessage,
|
|
701
|
-
onClose,
|
|
702
|
-
onReopen,
|
|
703
|
-
onActionCollapsed,
|
|
704
|
-
id
|
|
705
|
-
}) {
|
|
706
|
-
const optimistic = isOptimistic(ticket);
|
|
707
|
-
const rowRef = useRef(null);
|
|
708
|
-
const handleClick = useCallback2(() => {
|
|
709
|
-
onToggle(ticket.id);
|
|
710
|
-
scrollElementIntoView(rowRef.current, {
|
|
711
|
-
adjustTargetY: (raw) => {
|
|
712
|
-
if (!rowRef.current) return raw;
|
|
713
|
-
const expandedDrawer = document.querySelector(
|
|
714
|
-
'div[id^="ticket-drawer-"]'
|
|
715
|
-
);
|
|
716
|
-
if (!(expandedDrawer instanceof HTMLElement)) return raw;
|
|
717
|
-
const drawerRect = expandedDrawer.getBoundingClientRect();
|
|
718
|
-
const myRect = rowRef.current.getBoundingClientRect();
|
|
719
|
-
if (drawerRect.bottom > myRect.top) return raw;
|
|
720
|
-
return raw - drawerRect.height;
|
|
721
|
-
}
|
|
722
|
-
});
|
|
723
|
-
}, [onToggle, ticket.id]);
|
|
724
|
-
const tileData = {
|
|
725
|
-
id: ticket.id,
|
|
726
|
-
title: ticket.subject ?? "(untitled)",
|
|
727
|
-
ticketNumber: `#${ticket.external_id}`,
|
|
728
|
-
status: ticket.status ?? "OPEN",
|
|
729
|
-
// Surface the HubSpot pipeline stage label ("New" / "Closed" /
|
|
730
|
-
// "Waiting on contact" / "Waiting on version release") instead of
|
|
731
|
-
// the canonical "Active"/"Resolved" default. The variant + check
|
|
732
|
-
// icon still come from `status` (CLOSED → check; OPEN → no check),
|
|
733
|
-
// so the badge accurately reflects "Closed" with a checkmark.
|
|
734
|
-
statusLabel: ticket.pipeline_stage_label ?? void 0,
|
|
735
|
-
category: ticket.customer_company ?? void 0,
|
|
736
|
-
timeAgo: ticket.hubspot_updated_at ? formatRelativeTime(ticket.hubspot_updated_at) : void 0,
|
|
737
|
-
// Linked-work chip: surfaced whenever the ticket has a linked
|
|
738
|
-
// ClickUp task. Uses the linked task's own status so the chip text
|
|
739
|
-
// reads "Working" / "Waiting on version release" / etc. — useful
|
|
740
|
-
// signal pre-expand. Falls back to a generic "Linked work" label
|
|
741
|
-
// when the task exists but its status hasn't synced yet.
|
|
742
|
-
linkedTaskLabel: ticket.clickup ? ticket.clickup.status ? ticket.clickup.status.replace(/\b\w/g, (c) => c.toUpperCase()) : "Linked work" : void 0
|
|
743
|
-
};
|
|
744
|
-
return /* @__PURE__ */ jsx5("div", { ref: rowRef, id, className: "scroll-mt-24", children: /* @__PURE__ */ jsxs4(
|
|
745
|
-
Collapsible,
|
|
746
|
-
{
|
|
747
|
-
open: expanded && !optimistic,
|
|
748
|
-
className: "border-b border-ods-border last:border-b-0",
|
|
749
|
-
children: [
|
|
750
|
-
/* @__PURE__ */ jsx5(
|
|
751
|
-
ChatTicketItem,
|
|
752
|
-
{
|
|
753
|
-
ticket: tileData,
|
|
754
|
-
onClick: optimistic ? void 0 : handleClick,
|
|
755
|
-
"aria-expanded": expanded && !optimistic,
|
|
756
|
-
"aria-controls": `ticket-drawer-${ticket.id}`
|
|
757
|
-
}
|
|
758
|
-
),
|
|
759
|
-
/* @__PURE__ */ jsx5(
|
|
760
|
-
CollapsibleContent2,
|
|
761
|
-
{
|
|
762
|
-
id: `ticket-drawer-${ticket.id}`,
|
|
763
|
-
className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
|
|
764
|
-
children: /* @__PURE__ */ jsx5(
|
|
765
|
-
TicketDetailDrawer,
|
|
766
|
-
{
|
|
767
|
-
ticket,
|
|
768
|
-
busy,
|
|
769
|
-
supportSystemDown,
|
|
770
|
-
onSendMessage,
|
|
771
|
-
onClose,
|
|
772
|
-
onReopen,
|
|
773
|
-
onActionCollapsed
|
|
774
|
-
}
|
|
775
|
-
)
|
|
776
|
-
}
|
|
777
|
-
)
|
|
778
|
-
]
|
|
779
|
-
}
|
|
780
|
-
) });
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// src/components/tickets/hooks/use-tickets-list.ts
|
|
784
|
-
import { useQuery as useQuery2 } from "@tanstack/react-query";
|
|
785
|
-
var FIND_TICKET_ENDPOINT = "/api/chat/agent/find-ticket";
|
|
786
|
-
var DEFAULT_PAGE_SIZE = 20;
|
|
787
|
-
function useTicketsList(filters) {
|
|
788
|
-
const findTicketEndpoint = useRequiredChatRuntime().endpoints.findTicketUrl ?? FIND_TICKET_ENDPOINT;
|
|
789
|
-
const customerEmail = filters.customerEmail;
|
|
790
|
-
const search = (filters.search ?? "").trim();
|
|
791
|
-
const status = (filters.status ?? "").trim().toLowerCase();
|
|
792
|
-
const statusFilter = status && status !== "all" ? status : "";
|
|
793
|
-
const page = Math.max(1, Math.floor(filters.page ?? 1) || 1);
|
|
794
|
-
const pageSize = Math.max(1, Math.min(100, Math.floor(filters.pageSize ?? DEFAULT_PAGE_SIZE) || DEFAULT_PAGE_SIZE));
|
|
795
|
-
const enabled = !!customerEmail;
|
|
796
|
-
const identityKey = customerEmail || "anon";
|
|
797
|
-
const refetchInterval = filters.refetchInterval ?? false;
|
|
798
|
-
const query = useQuery2({
|
|
799
|
-
queryKey: ["tickets", "self", identityKey, search, statusFilter, page, pageSize],
|
|
800
|
-
enabled,
|
|
801
|
-
// Caches OFF — every mount + every focus + every navigation triggers
|
|
802
|
-
// a fresh fetch. The ticket data is the customer's own list of
|
|
803
|
-
// tickets they expect to be live (sync agents reply, statuses flip,
|
|
804
|
-
// new comments arrive); a stale window of any size is worse than
|
|
805
|
-
// a sub-second refetch.
|
|
806
|
-
staleTime: 0,
|
|
807
|
-
gcTime: 0,
|
|
808
|
-
refetchOnMount: "always",
|
|
809
|
-
refetchOnWindowFocus: true,
|
|
810
|
-
// Live status: poll while the caller opts in (drawer open). Defaults to
|
|
811
|
-
// false. `refetchIntervalInBackground` stays false (the default) so polling
|
|
812
|
-
// pauses on a hidden tab — no wasted requests when the user tabs away.
|
|
813
|
-
refetchInterval,
|
|
814
|
-
queryFn: async () => {
|
|
815
|
-
const body = {
|
|
816
|
-
query: search,
|
|
817
|
-
page,
|
|
818
|
-
pageSize
|
|
819
|
-
};
|
|
820
|
-
if (statusFilter) body.status = statusFilter;
|
|
821
|
-
const response = await embedAuthedFetch(findTicketEndpoint, {
|
|
822
|
-
method: "POST",
|
|
823
|
-
body: JSON.stringify(body)
|
|
824
|
-
});
|
|
825
|
-
if (!response.ok) {
|
|
826
|
-
const text = await response.text().catch(() => "");
|
|
827
|
-
throw new Error(`find-ticket failed: ${response.status} ${text.slice(0, 200)}`);
|
|
828
|
-
}
|
|
829
|
-
return await response.json();
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
const data = query.data;
|
|
833
|
-
const totalCount = data?.totalCount ?? data?.count ?? (data?.tickets?.length ?? 0);
|
|
834
|
-
const echoedPage = data?.page ?? page;
|
|
835
|
-
const echoedPageSize = data?.pageSize ?? pageSize;
|
|
836
|
-
const totalPages = data?.totalPages ?? Math.max(1, Math.ceil(totalCount / echoedPageSize));
|
|
837
|
-
return {
|
|
838
|
-
tickets: data?.tickets ?? [],
|
|
839
|
-
// Loading-state-truth = `data === undefined`. TanStack v5's
|
|
840
|
-
// `isPending` / `isLoading` flags can be `false` in transient
|
|
841
|
-
// windows where the query is enabled-but-fetch-not-yet-fired
|
|
842
|
-
// OR where stale-data exists from a sibling cache slot — both
|
|
843
|
-
// produced the EmptyState flash on /tickets first load. Treating
|
|
844
|
-
// "no data for THIS query slot yet" as the universal loading
|
|
845
|
-
// signal can't lie:
|
|
846
|
-
// - Initial render after enabled flips: data === undefined → load
|
|
847
|
-
// - Background refetch with existing data: data !== undefined → no load
|
|
848
|
-
// - Filter-change refetch landing on empty results: data?.tickets===[]
|
|
849
|
-
// + isFetching → bridge skeleton (the `||` branch)
|
|
850
|
-
// Loading-state-truth = `data === undefined`. TanStack v5's
|
|
851
|
-
// `isPending` / `isLoading` flags can be `false` in transient
|
|
852
|
-
// windows where the query is enabled-but-fetch-not-yet-fired
|
|
853
|
-
// OR where stale-data exists from a sibling cache slot. Treating
|
|
854
|
-
// "no data for THIS query slot yet" as the universal loading
|
|
855
|
-
// signal can't lie:
|
|
856
|
-
// - Initial render after enabled flips: data === undefined → load
|
|
857
|
-
// - Background refetch with existing data: data !== undefined → no load
|
|
858
|
-
// - Filter-change refetch landing on empty results: data?.tickets===[]
|
|
859
|
-
// + isFetching → bridge skeleton (the `||` branch)
|
|
860
|
-
isLoading: enabled && (data === void 0 || query.isFetching && (data?.tickets ?? []).length === 0),
|
|
861
|
-
isFetching: query.isFetching,
|
|
862
|
-
error: query.error ?? null,
|
|
863
|
-
refetch: () => {
|
|
864
|
-
void query.refetch();
|
|
865
|
-
},
|
|
866
|
-
lastUpdatedAt: query.dataUpdatedAt || null,
|
|
867
|
-
totalCount,
|
|
868
|
-
page: echoedPage,
|
|
869
|
-
pageSize: echoedPageSize,
|
|
870
|
-
totalPages
|
|
871
|
-
};
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// src/components/tickets/hooks/use-ticket-actions.ts
|
|
875
|
-
import { useCallback as useCallback3, useEffect, useMemo, useRef as useRef2, useState as useState3 } from "react";
|
|
876
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
877
|
-
var TICKET_ACTION_ENDPOINT = "/api/chat/agent/ticket-action";
|
|
878
|
-
var REPLY_BANNER_CODES = /* @__PURE__ */ new Set([
|
|
879
|
-
"HUBSPOT_5XX",
|
|
880
|
-
"HUBSPOT_400_VALIDATION",
|
|
881
|
-
"HUBSPOT_404_THREAD",
|
|
882
|
-
"HUBSPOT_REPLY_UNKNOWN"
|
|
883
|
-
]);
|
|
884
|
-
var MIRROR_SYNC_BACKOFF_MS = [3e3, 6e3, 12e3];
|
|
885
|
-
function useTicketActions(options) {
|
|
886
|
-
const queryClient = useQueryClient();
|
|
887
|
-
const ticketActionEndpoint = useRequiredChatRuntime().endpoints.ticketActionUrl ?? TICKET_ACTION_ENDPOINT;
|
|
888
|
-
const { prependOptimistic, removeOptimistic, removeTicketFromCache, toast: toast2, onSupportSystemDown } = options;
|
|
889
|
-
const formInFlightRef = useRef2(false);
|
|
890
|
-
const [isSubmittingForm, setIsSubmittingForm] = useState3(false);
|
|
891
|
-
const busyRowsRef = useRef2(/* @__PURE__ */ new Set());
|
|
892
|
-
const [busyRows, setBusyRows] = useState3(() => /* @__PURE__ */ new Set());
|
|
893
|
-
const setRowBusy = useCallback3((id, busy) => {
|
|
894
|
-
if (busy) busyRowsRef.current.add(id);
|
|
895
|
-
else busyRowsRef.current.delete(id);
|
|
896
|
-
setBusyRows(new Set(busyRowsRef.current));
|
|
897
|
-
}, []);
|
|
898
|
-
const isRowBusy = useCallback3((id) => busyRows.has(id), [busyRows]);
|
|
899
|
-
const [replyErrorByTicket, setReplyErrorByTicket] = useState3(() => /* @__PURE__ */ new Map());
|
|
900
|
-
const setReplyError = useCallback3(
|
|
901
|
-
(externalId, mapped) => {
|
|
902
|
-
setReplyErrorByTicket((prev) => {
|
|
903
|
-
const next = new Map(prev);
|
|
904
|
-
if (mapped) next.set(externalId, mapped);
|
|
905
|
-
else next.delete(externalId);
|
|
906
|
-
return next;
|
|
907
|
-
});
|
|
908
|
-
},
|
|
909
|
-
[]
|
|
910
|
-
);
|
|
911
|
-
const replyErrorFor = useCallback3(
|
|
912
|
-
(externalId) => replyErrorByTicket.get(externalId) ?? null,
|
|
913
|
-
[replyErrorByTicket]
|
|
914
|
-
);
|
|
915
|
-
const clearReplyError = useCallback3(
|
|
916
|
-
(externalId) => setReplyError(externalId, null),
|
|
917
|
-
[setReplyError]
|
|
918
|
-
);
|
|
919
|
-
const watcherControllersRef = useRef2(/* @__PURE__ */ new Map());
|
|
920
|
-
useEffect(() => {
|
|
921
|
-
return () => {
|
|
922
|
-
for (const controller of watcherControllersRef.current.values()) {
|
|
923
|
-
controller.abort();
|
|
924
|
-
}
|
|
925
|
-
watcherControllersRef.current.clear();
|
|
926
|
-
};
|
|
927
|
-
}, []);
|
|
928
|
-
const queueRef = useRef2(Promise.resolve());
|
|
929
|
-
const enqueue = useCallback3((work) => {
|
|
930
|
-
const next = queueRef.current.then(work, work);
|
|
931
|
-
queueRef.current = next.catch(() => void 0);
|
|
932
|
-
return next;
|
|
933
|
-
}, []);
|
|
934
|
-
const executeTicketAction = useCallback3(
|
|
935
|
-
async (toolName, args) => {
|
|
936
|
-
const res = await embedAuthedFetch(ticketActionEndpoint, {
|
|
937
|
-
method: "POST",
|
|
938
|
-
body: JSON.stringify({ tool_name: toolName, args })
|
|
939
|
-
});
|
|
940
|
-
const body = await res.json().catch(() => ({}));
|
|
941
|
-
if (!res.ok) {
|
|
942
|
-
const code = resolveErrorCode(body.code, res.status);
|
|
943
|
-
const message = body.error || `${toolName} failed (${res.status})`;
|
|
944
|
-
throw new TicketActionFailure(code, message, res);
|
|
945
|
-
}
|
|
946
|
-
return body;
|
|
947
|
-
},
|
|
948
|
-
[ticketActionEndpoint]
|
|
949
|
-
);
|
|
950
|
-
const watchMirrorSync = useCallback3(
|
|
951
|
-
(placeholderId, expectedTicketId) => {
|
|
952
|
-
const prior = watcherControllersRef.current.get(placeholderId);
|
|
953
|
-
if (prior) prior.abort();
|
|
954
|
-
const controller = new AbortController();
|
|
955
|
-
watcherControllersRef.current.set(placeholderId, controller);
|
|
956
|
-
const schedule = async () => {
|
|
957
|
-
try {
|
|
958
|
-
for (let i = 0; i < MIRROR_SYNC_BACKOFF_MS.length; i++) {
|
|
959
|
-
if (controller.signal.aborted) return;
|
|
960
|
-
await new Promise((resolve) => {
|
|
961
|
-
const t = setTimeout(resolve, MIRROR_SYNC_BACKOFF_MS[i]);
|
|
962
|
-
controller.signal.addEventListener(
|
|
963
|
-
"abort",
|
|
964
|
-
() => {
|
|
965
|
-
clearTimeout(t);
|
|
966
|
-
resolve();
|
|
967
|
-
},
|
|
968
|
-
{ once: true }
|
|
969
|
-
);
|
|
970
|
-
});
|
|
971
|
-
if (controller.signal.aborted) return;
|
|
972
|
-
await queryClient.invalidateQueries({ queryKey: ["tickets"] });
|
|
973
|
-
if (expectedTicketId && cacheContainsTicket(queryClient, expectedTicketId)) {
|
|
974
|
-
removeOptimistic(placeholderId);
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
if (!controller.signal.aborted) {
|
|
979
|
-
removeOptimistic(placeholderId);
|
|
980
|
-
toast2({
|
|
981
|
-
title: "Couldn't confirm ticket",
|
|
982
|
-
description: "If the ticket doesn't appear shortly, please contact support.",
|
|
983
|
-
variant: "destructive"
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
} finally {
|
|
987
|
-
if (watcherControllersRef.current.get(placeholderId) === controller) {
|
|
988
|
-
watcherControllersRef.current.delete(placeholderId);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
void schedule();
|
|
993
|
-
},
|
|
994
|
-
[queryClient, removeOptimistic, toast2]
|
|
995
|
-
);
|
|
996
|
-
const lastUpdateErrorRef = useRef2(null);
|
|
997
|
-
const surfaceError = useCallback3(
|
|
998
|
-
(err, action) => {
|
|
999
|
-
const mapped = mapTicketActionError(err);
|
|
1000
|
-
lastUpdateErrorRef.current = mapped;
|
|
1001
|
-
if (mapped.supportSystemDown) onSupportSystemDown();
|
|
1002
|
-
toast2({
|
|
1003
|
-
title: `Could not ${action}`,
|
|
1004
|
-
description: mapped.message,
|
|
1005
|
-
variant: "destructive"
|
|
1006
|
-
});
|
|
1007
|
-
return mapped;
|
|
1008
|
-
},
|
|
1009
|
-
[toast2, onSupportSystemDown]
|
|
1010
|
-
);
|
|
1011
|
-
const submitTicket = useCallback3(
|
|
1012
|
-
async (input) => {
|
|
1013
|
-
if (formInFlightRef.current) return false;
|
|
1014
|
-
formInFlightRef.current = true;
|
|
1015
|
-
setIsSubmittingForm(true);
|
|
1016
|
-
const placeholderId = `temp-${cryptoRandomId()}`;
|
|
1017
|
-
const placeholder = {
|
|
1018
|
-
id: placeholderId,
|
|
1019
|
-
external_id: "Pending sync\u2026",
|
|
1020
|
-
subject: input.subject.trim(),
|
|
1021
|
-
preview: input.content.trim().slice(0, 400),
|
|
1022
|
-
body: input.content.trim(),
|
|
1023
|
-
status: "OPEN",
|
|
1024
|
-
pipeline_stage_label: "New",
|
|
1025
|
-
clickup_task_id: null,
|
|
1026
|
-
clickup: null,
|
|
1027
|
-
priority: null,
|
|
1028
|
-
customer_emails: [],
|
|
1029
|
-
customer_company: null,
|
|
1030
|
-
// Optimistic placeholder has no resolved HubSpot contact yet
|
|
1031
|
-
// — the real ticket row replaces this within a couple of
|
|
1032
|
-
// seconds via the mirror refetch. Drawer uses live chat
|
|
1033
|
-
// identity for own-replies during this window anyway.
|
|
1034
|
-
customer_name: null,
|
|
1035
|
-
// No assignee until the real ticket lands. Drawer renders
|
|
1036
|
-
// "Unassigned" for this brief window.
|
|
1037
|
-
assigned_to: null,
|
|
1038
|
-
assignedOwner: null,
|
|
1039
|
-
hubspot_updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1040
|
-
_optimistic: true
|
|
1041
|
-
};
|
|
1042
|
-
prependOptimistic(placeholder);
|
|
1043
|
-
try {
|
|
1044
|
-
return await enqueue(async () => {
|
|
1045
|
-
const result = await executeTicketAction("create_ticket", {
|
|
1046
|
-
subject: input.subject.trim(),
|
|
1047
|
-
content: input.content.trim(),
|
|
1048
|
-
...input.attachments?.length ? { attachments: input.attachments } : {}
|
|
1049
|
-
});
|
|
1050
|
-
if (result.mirror_synced === false) {
|
|
1051
|
-
toast2(TOAST_COPY.open_mirror_pending);
|
|
1052
|
-
watchMirrorSync(placeholderId, result.ticket_id);
|
|
1053
|
-
} else {
|
|
1054
|
-
toast2(TOAST_COPY.open_success);
|
|
1055
|
-
await queryClient.invalidateQueries({ queryKey: ["tickets"] });
|
|
1056
|
-
removeOptimistic(placeholderId);
|
|
1057
|
-
}
|
|
1058
|
-
return true;
|
|
1059
|
-
});
|
|
1060
|
-
} catch (err) {
|
|
1061
|
-
removeOptimistic(placeholderId);
|
|
1062
|
-
surfaceError(err, "open ticket");
|
|
1063
|
-
return false;
|
|
1064
|
-
} finally {
|
|
1065
|
-
formInFlightRef.current = false;
|
|
1066
|
-
setIsSubmittingForm(false);
|
|
1067
|
-
}
|
|
1068
|
-
},
|
|
1069
|
-
[
|
|
1070
|
-
enqueue,
|
|
1071
|
-
executeTicketAction,
|
|
1072
|
-
prependOptimistic,
|
|
1073
|
-
removeOptimistic,
|
|
1074
|
-
queryClient,
|
|
1075
|
-
toast2,
|
|
1076
|
-
watchMirrorSync,
|
|
1077
|
-
surfaceError
|
|
1078
|
-
]
|
|
1079
|
-
);
|
|
1080
|
-
const updateTicket = useCallback3(
|
|
1081
|
-
async (ticket, serverArgs, successCopy, action) => {
|
|
1082
|
-
if (busyRowsRef.current.has(ticket.id)) return false;
|
|
1083
|
-
setRowBusy(ticket.id, true);
|
|
1084
|
-
try {
|
|
1085
|
-
return await enqueue(async () => {
|
|
1086
|
-
await executeTicketAction("update_ticket", {
|
|
1087
|
-
...serverArgs,
|
|
1088
|
-
ticket_id: ticket.external_id
|
|
1089
|
-
});
|
|
1090
|
-
toast2(successCopy);
|
|
1091
|
-
const statusUpdate = serverArgs.status ?? null;
|
|
1092
|
-
if (statusUpdate) {
|
|
1093
|
-
queryClient.setQueriesData(
|
|
1094
|
-
{ queryKey: ["tickets"] },
|
|
1095
|
-
(prev) => {
|
|
1096
|
-
if (!prev || !Array.isArray(prev.tickets)) return prev;
|
|
1097
|
-
let mutated = false;
|
|
1098
|
-
const nextTickets = prev.tickets.map((t) => {
|
|
1099
|
-
if (t.id !== ticket.id || t.status === statusUpdate) return t;
|
|
1100
|
-
mutated = true;
|
|
1101
|
-
return { ...t, status: statusUpdate };
|
|
1102
|
-
});
|
|
1103
|
-
return mutated ? { ...prev, tickets: nextTickets } : prev;
|
|
1104
|
-
}
|
|
1105
|
-
);
|
|
1106
|
-
}
|
|
1107
|
-
await queryClient.invalidateQueries({ queryKey: ["ticket-engagements"] });
|
|
1108
|
-
return true;
|
|
1109
|
-
});
|
|
1110
|
-
} catch (err) {
|
|
1111
|
-
const mapped = surfaceError(err, action);
|
|
1112
|
-
if (mapped.removeRowFromCache) {
|
|
1113
|
-
removeTicketFromCache(ticket.id);
|
|
1114
|
-
}
|
|
1115
|
-
return false;
|
|
1116
|
-
} finally {
|
|
1117
|
-
setRowBusy(ticket.id, false);
|
|
1118
|
-
}
|
|
1119
|
-
},
|
|
1120
|
-
// `busyRowsRef` is read via .current — needs no dep entry. `busyRows`
|
|
1121
|
-
// state isn't read inside this callback (only by `isRowBusy` selector
|
|
1122
|
-
// outside), so listing it would churn the closure on every flag flip
|
|
1123
|
-
// and cascade-recreate addNote/closeTicket/etc.
|
|
1124
|
-
[setRowBusy, enqueue, executeTicketAction, queryClient, toast2, surfaceError, removeTicketFromCache]
|
|
1125
|
-
);
|
|
1126
|
-
const sendMessage = useCallback3(
|
|
1127
|
-
async (ticket, text, attachments) => {
|
|
1128
|
-
const trimmed = text.trim();
|
|
1129
|
-
const hasText = trimmed.length > 0;
|
|
1130
|
-
const hasFiles = attachments.length > 0;
|
|
1131
|
-
if (!hasText && !hasFiles) return false;
|
|
1132
|
-
lastUpdateErrorRef.current = null;
|
|
1133
|
-
const ok = await updateTicket(
|
|
1134
|
-
ticket,
|
|
1135
|
-
{
|
|
1136
|
-
...hasText ? { content_addendum: trimmed } : {},
|
|
1137
|
-
...hasFiles ? { attachments } : {}
|
|
1138
|
-
},
|
|
1139
|
-
TOAST_COPY.comment_success,
|
|
1140
|
-
"send message"
|
|
1141
|
-
);
|
|
1142
|
-
if (ok) {
|
|
1143
|
-
clearReplyError(ticket.external_id);
|
|
1144
|
-
} else {
|
|
1145
|
-
const mapped = lastUpdateErrorRef.current;
|
|
1146
|
-
if (mapped && REPLY_BANNER_CODES.has(mapped.code)) {
|
|
1147
|
-
setReplyError(ticket.external_id, mapped);
|
|
1148
|
-
}
|
|
1149
|
-
lastUpdateErrorRef.current = null;
|
|
1150
|
-
}
|
|
1151
|
-
return ok;
|
|
1152
|
-
},
|
|
1153
|
-
[updateTicket, clearReplyError, setReplyError]
|
|
1154
|
-
);
|
|
1155
|
-
const closeTicket = useCallback3(
|
|
1156
|
-
(ticket, resolution) => updateTicket(
|
|
1157
|
-
ticket,
|
|
1158
|
-
{
|
|
1159
|
-
status: "CLOSED",
|
|
1160
|
-
...resolution?.trim() ? { resolution: resolution.trim() } : {}
|
|
1161
|
-
},
|
|
1162
|
-
TOAST_COPY.close_success,
|
|
1163
|
-
"close ticket"
|
|
1164
|
-
),
|
|
1165
|
-
[updateTicket]
|
|
1166
|
-
);
|
|
1167
|
-
const reopenTicket = useCallback3(
|
|
1168
|
-
(ticket) => updateTicket(ticket, { status: "OPEN" }, TOAST_COPY.reopen_success, "reopen ticket"),
|
|
1169
|
-
[updateTicket]
|
|
1170
|
-
);
|
|
1171
|
-
return useMemo(
|
|
1172
|
-
() => ({
|
|
1173
|
-
submitTicket,
|
|
1174
|
-
sendMessage,
|
|
1175
|
-
closeTicket,
|
|
1176
|
-
reopenTicket,
|
|
1177
|
-
isSubmittingForm,
|
|
1178
|
-
isRowBusy,
|
|
1179
|
-
replyErrorFor,
|
|
1180
|
-
clearReplyError
|
|
1181
|
-
}),
|
|
1182
|
-
[
|
|
1183
|
-
submitTicket,
|
|
1184
|
-
sendMessage,
|
|
1185
|
-
closeTicket,
|
|
1186
|
-
reopenTicket,
|
|
1187
|
-
isSubmittingForm,
|
|
1188
|
-
isRowBusy,
|
|
1189
|
-
replyErrorFor,
|
|
1190
|
-
clearReplyError
|
|
1191
|
-
]
|
|
1192
|
-
);
|
|
1193
|
-
}
|
|
1194
|
-
var TicketActionFailure = class extends Error {
|
|
1195
|
-
constructor(code, message, response) {
|
|
1196
|
-
super(message);
|
|
1197
|
-
this.code = code;
|
|
1198
|
-
this.response = response;
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
function mapTicketActionError(err) {
|
|
1202
|
-
if (err instanceof TicketActionFailure) {
|
|
1203
|
-
switch (err.code) {
|
|
1204
|
-
case "PROPOSAL_NOT_CLAIMABLE":
|
|
1205
|
-
return {
|
|
1206
|
-
code: err.code,
|
|
1207
|
-
message: "This action was already processed.",
|
|
1208
|
-
supportSystemDown: false,
|
|
1209
|
-
removeRowFromCache: false
|
|
1210
|
-
};
|
|
1211
|
-
case "TICKET_NOT_FOUND":
|
|
1212
|
-
return {
|
|
1213
|
-
code: err.code,
|
|
1214
|
-
message: "This ticket is no longer available.",
|
|
1215
|
-
supportSystemDown: false,
|
|
1216
|
-
removeRowFromCache: true
|
|
1217
|
-
};
|
|
1218
|
-
case "TICKET_OWNERSHIP_DENIED":
|
|
1219
|
-
return {
|
|
1220
|
-
code: err.code,
|
|
1221
|
-
message: "You can only act on tickets you opened.",
|
|
1222
|
-
supportSystemDown: false,
|
|
1223
|
-
removeRowFromCache: false
|
|
1224
|
-
};
|
|
1225
|
-
case "HUBSPOT_DISCONNECTED":
|
|
1226
|
-
return {
|
|
1227
|
-
code: err.code,
|
|
1228
|
-
message: "Support system temporarily unavailable.",
|
|
1229
|
-
supportSystemDown: true,
|
|
1230
|
-
removeRowFromCache: false
|
|
1231
|
-
};
|
|
1232
|
-
case "RATE_LIMITED": {
|
|
1233
|
-
const retryAfterRaw = err.response?.headers.get("Retry-After");
|
|
1234
|
-
const retryAfterSeconds = retryAfterRaw ? parseInt(retryAfterRaw, 10) : void 0;
|
|
1235
|
-
return {
|
|
1236
|
-
code: err.code,
|
|
1237
|
-
message: retryAfterSeconds ? `Too many actions. Try again in ${retryAfterSeconds}s.` : "Too many actions. Try again shortly.",
|
|
1238
|
-
supportSystemDown: false,
|
|
1239
|
-
removeRowFromCache: false,
|
|
1240
|
-
...retryAfterSeconds ? { retryAfterSeconds } : {}
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
case "INVALID_TOOL_ARGS":
|
|
1244
|
-
return {
|
|
1245
|
-
code: err.code,
|
|
1246
|
-
message: "Your input was rejected. Please review and try again.",
|
|
1247
|
-
supportSystemDown: false,
|
|
1248
|
-
removeRowFromCache: false
|
|
1249
|
-
};
|
|
1250
|
-
case "HUBSPOT_5XX":
|
|
1251
|
-
return {
|
|
1252
|
-
code: err.code,
|
|
1253
|
-
message: "We couldn't reach the support system. Your reply wasn't sent \u2014 please retry in a moment.",
|
|
1254
|
-
supportSystemDown: false,
|
|
1255
|
-
removeRowFromCache: false
|
|
1256
|
-
};
|
|
1257
|
-
case "HUBSPOT_400_VALIDATION":
|
|
1258
|
-
return {
|
|
1259
|
-
code: err.code,
|
|
1260
|
-
message: "Your reply was rejected. Please rephrase or remove unsupported content and try again.",
|
|
1261
|
-
supportSystemDown: false,
|
|
1262
|
-
removeRowFromCache: false
|
|
1263
|
-
};
|
|
1264
|
-
case "HUBSPOT_404_THREAD":
|
|
1265
|
-
return {
|
|
1266
|
-
code: err.code,
|
|
1267
|
-
message: "This conversation is no longer accepting replies. Open a new ticket to continue.",
|
|
1268
|
-
supportSystemDown: false,
|
|
1269
|
-
removeRowFromCache: false
|
|
1270
|
-
};
|
|
1271
|
-
case "HUBSPOT_REPLY_UNKNOWN":
|
|
1272
|
-
return {
|
|
1273
|
-
code: err.code,
|
|
1274
|
-
message: "Your reply didn't go through. Please retry.",
|
|
1275
|
-
supportSystemDown: false,
|
|
1276
|
-
removeRowFromCache: false
|
|
1277
|
-
};
|
|
1278
|
-
default:
|
|
1279
|
-
return {
|
|
1280
|
-
code: "UNKNOWN",
|
|
1281
|
-
message: err.message || "Something went wrong. Please try again.",
|
|
1282
|
-
supportSystemDown: false,
|
|
1283
|
-
removeRowFromCache: false
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
return {
|
|
1288
|
-
code: "UNKNOWN",
|
|
1289
|
-
message: err instanceof Error ? err.message : "Something went wrong. Please try again.",
|
|
1290
|
-
supportSystemDown: false,
|
|
1291
|
-
removeRowFromCache: false
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
function cryptoRandomId() {
|
|
1295
|
-
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
1296
|
-
return crypto.randomUUID();
|
|
1297
|
-
}
|
|
1298
|
-
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1299
|
-
}
|
|
1300
|
-
function cacheContainsTicket(queryClient, expectedTicketId) {
|
|
1301
|
-
const entries = queryClient.getQueriesData({
|
|
1302
|
-
queryKey: ["tickets"]
|
|
1303
|
-
});
|
|
1304
|
-
for (const [, data] of entries) {
|
|
1305
|
-
if (data && Array.isArray(data.tickets) && data.tickets.some((t) => t.external_id === expectedTicketId)) {
|
|
1306
|
-
return true;
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
return false;
|
|
1310
|
-
}
|
|
1311
|
-
function resolveErrorCode(bodyCode, status) {
|
|
1312
|
-
if (bodyCode) return bodyCode;
|
|
1313
|
-
if (status === 429) return "RATE_LIMITED";
|
|
1314
|
-
if (status === 412) return "HUBSPOT_DISCONNECTED";
|
|
1315
|
-
return "UNKNOWN";
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// src/components/tickets/ticket-center.tsx
|
|
1319
|
-
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1320
|
-
function TicketCenter({ toast: toast2 = toast } = {}) {
|
|
1321
|
-
const identity = useChatIdentity();
|
|
1322
|
-
if (identity.isLoading) {
|
|
1323
|
-
return /* @__PURE__ */ jsx6(TicketCenterSkeleton, {});
|
|
1324
|
-
}
|
|
1325
|
-
if (identity.authTier === "anon" || !identity.user?.email) {
|
|
1326
|
-
return /* @__PURE__ */ jsx6(
|
|
1327
|
-
EmptyState,
|
|
1328
|
-
{
|
|
1329
|
-
type: "generic",
|
|
1330
|
-
title: "Sign in to manage tickets",
|
|
1331
|
-
description: "View, open, and follow up on support tickets after signing in.",
|
|
1332
|
-
showCTA: false
|
|
1333
|
-
}
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
return /* @__PURE__ */ jsx6(
|
|
1337
|
-
TicketCenterAuthed,
|
|
1338
|
-
{
|
|
1339
|
-
toast: toast2,
|
|
1340
|
-
sessionEmail: identity.user.email
|
|
1341
|
-
}
|
|
1342
|
-
);
|
|
1343
|
-
}
|
|
1344
|
-
function TicketCenterAuthed({
|
|
1345
|
-
toast: toast2,
|
|
1346
|
-
sessionEmail
|
|
1347
|
-
}) {
|
|
1348
|
-
const queryClient = useQueryClient2();
|
|
1349
|
-
const { tickets, isLoading, isFetching, refetch, lastUpdatedAt } = useTicketsList({
|
|
1350
|
-
customerEmail: sessionEmail
|
|
1351
|
-
});
|
|
1352
|
-
const [optimisticTickets, setOptimisticTickets] = useState4([]);
|
|
1353
|
-
const [expandedTicketId, setExpandedTicketId] = useState4(null);
|
|
1354
|
-
const [supportSystemDown, setSupportSystemDown] = useState4(false);
|
|
1355
|
-
const prependOptimistic = useCallback4((placeholder) => {
|
|
1356
|
-
setOptimisticTickets((prev) => [placeholder, ...prev]);
|
|
1357
|
-
}, []);
|
|
1358
|
-
const removeOptimistic = useCallback4((placeholderId) => {
|
|
1359
|
-
setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
|
|
1360
|
-
setExpandedTicketId((prev) => prev === placeholderId ? null : prev);
|
|
1361
|
-
}, []);
|
|
1362
|
-
const removeTicketFromCache = useCallback4(
|
|
1363
|
-
(ticketId) => {
|
|
1364
|
-
queryClient.setQueriesData(
|
|
1365
|
-
{ queryKey: ["tickets"] },
|
|
1366
|
-
(prev) => (prev ?? []).filter((t) => t.id !== ticketId)
|
|
1367
|
-
);
|
|
1368
|
-
setExpandedTicketId((prev) => prev === ticketId ? null : prev);
|
|
1369
|
-
},
|
|
1370
|
-
[queryClient]
|
|
1371
|
-
);
|
|
1372
|
-
const actions = useTicketActions({
|
|
1373
|
-
prependOptimistic,
|
|
1374
|
-
removeOptimistic,
|
|
1375
|
-
removeTicketFromCache,
|
|
1376
|
-
toast: toast2,
|
|
1377
|
-
onSupportSystemDown: () => setSupportSystemDown(true)
|
|
1378
|
-
});
|
|
1379
|
-
const toggleRow = useCallback4((id) => {
|
|
1380
|
-
setExpandedTicketId((prev) => prev === id ? null : id);
|
|
1381
|
-
}, []);
|
|
1382
|
-
const merged = [...optimisticTickets, ...tickets];
|
|
1383
|
-
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
|
|
1384
|
-
/* @__PURE__ */ jsx6(
|
|
1385
|
-
TicketOpenForm,
|
|
1386
|
-
{
|
|
1387
|
-
onSubmit: (input) => actions.submitTicket(input),
|
|
1388
|
-
isSubmitting: actions.isSubmittingForm,
|
|
1389
|
-
supportSystemDown
|
|
1390
|
-
}
|
|
1391
|
-
),
|
|
1392
|
-
/* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
|
|
1393
|
-
/* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3", children: [
|
|
1394
|
-
/* @__PURE__ */ jsx6("p", { className: "text-xs font-medium text-ods-text-secondary uppercase tracking-wider", children: "Your Current Tickets" }),
|
|
1395
|
-
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-3 text-xs text-ods-text-secondary", children: [
|
|
1396
|
-
lastUpdatedAt && /* @__PURE__ */ jsxs5("span", { children: [
|
|
1397
|
-
"Updated ",
|
|
1398
|
-
formatRelativeTime(new Date(lastUpdatedAt))
|
|
1399
|
-
] }),
|
|
1400
|
-
/* @__PURE__ */ jsx6(
|
|
1401
|
-
Button,
|
|
1402
|
-
{
|
|
1403
|
-
type: "button",
|
|
1404
|
-
variant: "transparent",
|
|
1405
|
-
size: "small",
|
|
1406
|
-
onClick: refetch,
|
|
1407
|
-
disabled: isFetching,
|
|
1408
|
-
"aria-label": "Refresh ticket list",
|
|
1409
|
-
leftIcon: /* @__PURE__ */ jsx6(RefreshCw, { className: "h-4 w-4" })
|
|
1410
|
-
}
|
|
1411
|
-
)
|
|
1412
|
-
] })
|
|
1413
|
-
] }),
|
|
1414
|
-
isLoading ? /* @__PURE__ */ jsx6(TicketListSkeleton, {}) : merged.length === 0 ? /* @__PURE__ */ jsx6(Card, { className: "p-6", children: /* @__PURE__ */ jsx6(
|
|
1415
|
-
EmptyState,
|
|
1416
|
-
{
|
|
1417
|
-
type: "generic",
|
|
1418
|
-
title: "No tickets yet",
|
|
1419
|
-
description: "Open one above to start the conversation.",
|
|
1420
|
-
showCTA: false
|
|
1421
|
-
}
|
|
1422
|
-
) }) : /* @__PURE__ */ jsx6(Card, { className: "overflow-hidden", children: merged.map((ticket) => /* @__PURE__ */ jsx6(
|
|
1423
|
-
TicketRow,
|
|
1424
|
-
{
|
|
1425
|
-
id: devSectionAnchorId("ticket", ticket.external_id),
|
|
1426
|
-
ticket,
|
|
1427
|
-
expanded: expandedTicketId === ticket.id,
|
|
1428
|
-
onToggle: toggleRow,
|
|
1429
|
-
busy: isOptimistic(ticket) ? false : actions.isRowBusy(ticket.id),
|
|
1430
|
-
supportSystemDown,
|
|
1431
|
-
onSendMessage: actions.sendMessage,
|
|
1432
|
-
onClose: actions.closeTicket,
|
|
1433
|
-
onReopen: actions.reopenTicket,
|
|
1434
|
-
onActionCollapsed: () => setExpandedTicketId(null)
|
|
1435
|
-
},
|
|
1436
|
-
ticket.id
|
|
1437
|
-
)) })
|
|
1438
|
-
] })
|
|
1439
|
-
] });
|
|
1440
|
-
}
|
|
1441
|
-
function TicketCenterSkeleton() {
|
|
1442
|
-
return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-6", children: [
|
|
1443
|
-
/* @__PURE__ */ jsxs5(Card, { className: "p-6", children: [
|
|
1444
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-7 w-48 mb-4" }),
|
|
1445
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-10 w-full mb-3" }),
|
|
1446
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-24 w-full" })
|
|
1447
|
-
] }),
|
|
1448
|
-
/* @__PURE__ */ jsx6(TicketListSkeleton, {})
|
|
1449
|
-
] });
|
|
1450
|
-
}
|
|
1451
|
-
function TicketListSkeleton() {
|
|
1452
|
-
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: [
|
|
1453
|
-
/* @__PURE__ */ jsxs5("div", { className: "flex-1 flex flex-col gap-2", children: [
|
|
1454
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-4 w-2/3" }),
|
|
1455
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-3 w-full" })
|
|
1456
|
-
] }),
|
|
1457
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-20" }),
|
|
1458
|
-
/* @__PURE__ */ jsx6(Skeleton, { className: "h-8 w-16" })
|
|
1459
|
-
] }, i)) });
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// src/components/tickets/help-center-list.tsx
|
|
1463
|
-
import { useCallback as useCallback6, useState as useState6 } from "react";
|
|
1464
|
-
import { useQueryClient as useQueryClient3 } from "@tanstack/react-query";
|
|
1465
|
-
init_unified_pagination();
|
|
1466
|
-
|
|
1467
|
-
// src/components/tickets/help-center-card.tsx
|
|
1468
|
-
import { useCallback as useCallback5, useEffect as useEffect2, useRef as useRef3 } from "react";
|
|
1469
|
-
import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1470
|
-
function HelpCenterCard({
|
|
1471
|
-
ticket,
|
|
1472
|
-
expanded,
|
|
1473
|
-
onToggle,
|
|
1474
|
-
busy,
|
|
1475
|
-
supportSystemDown,
|
|
1476
|
-
onSendMessage,
|
|
1477
|
-
onClose,
|
|
1478
|
-
onReopen,
|
|
1479
|
-
onActionCollapsed,
|
|
1480
|
-
replyError,
|
|
1481
|
-
onClearReplyError,
|
|
1482
|
-
id
|
|
1483
|
-
}) {
|
|
1484
|
-
const optimistic = isOptimistic(ticket);
|
|
1485
|
-
const rawStatus = (ticket.status ?? "OPEN").toUpperCase();
|
|
1486
|
-
const priority = (ticket.priority ?? "").toUpperCase();
|
|
1487
|
-
const relativeUpdated = ticket.hubspot_updated_at ? formatRelativeTime(ticket.hubspot_updated_at) : "recently";
|
|
1488
|
-
const title = (ticket.subject || "").trim() || "(untitled)";
|
|
1489
|
-
const subtitle = `UPDATED ${relativeUpdated}, #${ticket.external_id || "\u2014"}${ticket.pipeline_stage_label ? `, ${ticket.pipeline_stage_label}` : ""}`;
|
|
1490
|
-
const description = ticket.preview ?? ticket.body ?? "";
|
|
1491
|
-
const isExpandable = !optimistic;
|
|
1492
|
-
const isExpanded = expanded && isExpandable;
|
|
1493
|
-
const rowRef = useRef3(null);
|
|
1494
|
-
const handleClick = useCallback5(() => {
|
|
1495
|
-
onToggle(ticket.id);
|
|
1496
|
-
}, [onToggle, ticket.id]);
|
|
1497
|
-
useEffect2(() => {
|
|
1498
|
-
if (!isExpanded) return;
|
|
1499
|
-
const raf = requestAnimationFrame(() => {
|
|
1500
|
-
scrollElementIntoView(rowRef.current, {
|
|
1501
|
-
headerOffset: STICKY_HEADER_OFFSET_PX
|
|
1502
|
-
});
|
|
1503
|
-
});
|
|
1504
|
-
return () => cancelAnimationFrame(raf);
|
|
1505
|
-
}, [isExpanded]);
|
|
1506
|
-
const rightBadges = /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1507
|
-
/* @__PURE__ */ jsx7(
|
|
1508
|
-
StatusBadge,
|
|
1509
|
-
{
|
|
1510
|
-
text: rawStatus,
|
|
1511
|
-
colorScheme: getStatusColorScheme(rawStatus),
|
|
1512
|
-
variant: "card",
|
|
1513
|
-
className: "border border-ods-border"
|
|
1514
|
-
}
|
|
1515
|
-
),
|
|
1516
|
-
priority && /* @__PURE__ */ jsx7(
|
|
1517
|
-
StatusBadge,
|
|
1518
|
-
{
|
|
1519
|
-
text: priority,
|
|
1520
|
-
colorScheme: mapPriorityScheme(priority),
|
|
1521
|
-
variant: "card",
|
|
1522
|
-
className: "border border-ods-border"
|
|
1523
|
-
}
|
|
1524
|
-
)
|
|
1525
|
-
] });
|
|
1526
|
-
return /* @__PURE__ */ jsxs6(
|
|
1527
|
-
"div",
|
|
1528
|
-
{
|
|
1529
|
-
ref: rowRef,
|
|
1530
|
-
id,
|
|
1531
|
-
style: { scrollMarginTop: STICKY_HEADER_OFFSET_PX },
|
|
1532
|
-
className: `border-b border-ods-border last:border-b-0 ${optimistic ? "opacity-60" : ""}`,
|
|
1533
|
-
"aria-busy": optimistic || void 0,
|
|
1534
|
-
children: [
|
|
1535
|
-
/* @__PURE__ */ jsx7(
|
|
1536
|
-
"button",
|
|
1537
|
-
{
|
|
1538
|
-
type: "button",
|
|
1539
|
-
onClick: isExpandable ? handleClick : void 0,
|
|
1540
|
-
disabled: !isExpandable,
|
|
1541
|
-
"aria-expanded": isExpandable ? isExpanded : void 0,
|
|
1542
|
-
"aria-controls": isExpanded ? `help-center-drawer-${ticket.id}` : void 0,
|
|
1543
|
-
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",
|
|
1544
|
-
children: /* @__PURE__ */ jsx7(
|
|
1545
|
-
DevCardRowContent,
|
|
1546
|
-
{
|
|
1547
|
-
title,
|
|
1548
|
-
subtitle,
|
|
1549
|
-
description,
|
|
1550
|
-
emptyDescription: "No description provided",
|
|
1551
|
-
rightBadges
|
|
1552
|
-
}
|
|
1553
|
-
)
|
|
1554
|
-
}
|
|
1555
|
-
),
|
|
1556
|
-
isExpanded && /* @__PURE__ */ jsx7("div", { id: `help-center-drawer-${ticket.id}`, children: /* @__PURE__ */ jsx7(
|
|
1557
|
-
TicketDetailDrawer,
|
|
1558
|
-
{
|
|
1559
|
-
ticket,
|
|
1560
|
-
busy,
|
|
1561
|
-
supportSystemDown,
|
|
1562
|
-
onSendMessage,
|
|
1563
|
-
onClose,
|
|
1564
|
-
onReopen,
|
|
1565
|
-
onActionCollapsed,
|
|
1566
|
-
replyError,
|
|
1567
|
-
onClearReplyError
|
|
1568
|
-
}
|
|
1569
|
-
) })
|
|
1570
|
-
]
|
|
1571
|
-
}
|
|
1572
|
-
);
|
|
1573
|
-
}
|
|
1574
|
-
function mapPriorityScheme(priority) {
|
|
1575
|
-
if (priority === "HIGH" || priority === "URGENT") return "error";
|
|
1576
|
-
if (priority === "MEDIUM") return "warning";
|
|
1577
|
-
return "default";
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
// src/components/tickets/help-center-create-form.tsx
|
|
1581
|
-
import { useState as useState5 } from "react";
|
|
1582
|
-
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1583
|
-
var SUBJECT_MAX_CHARS = 200;
|
|
1584
|
-
function HelpCenterCreateFormSkeleton() {
|
|
1585
|
-
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: [
|
|
1586
|
-
/* @__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" }) }),
|
|
1587
|
-
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow space-y-4 md:space-y-6", children: [
|
|
1588
|
-
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1589
|
-
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1590
|
-
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1591
|
-
/* @__PURE__ */ jsx8("input", { type: "hidden", "aria-hidden": true }),
|
|
1592
|
-
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
|
|
1593
|
-
/* @__PURE__ */ jsx8("div", { className: "h-[27px] w-20 bg-ods-border rounded animate-pulse mb-1" }),
|
|
1594
|
-
/* @__PURE__ */ jsx8("div", { className: "h-12 w-full bg-ods-border rounded animate-pulse" })
|
|
1595
|
-
] }),
|
|
1596
|
-
/* @__PURE__ */ jsxs7("div", { className: "flex flex-col flex-grow", children: [
|
|
1597
|
-
/* @__PURE__ */ jsx8("div", { className: "h-[27px] w-32 bg-ods-border rounded animate-pulse mb-1" }),
|
|
1598
|
-
/* @__PURE__ */ jsx8("div", { className: "h-24 w-full bg-ods-border rounded animate-pulse flex-grow" })
|
|
1599
|
-
] }),
|
|
1600
|
-
/* @__PURE__ */ jsx8("div", { className: "flex flex-col gap-2", children: /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1601
|
-
/* @__PURE__ */ jsx8("div", { className: "h-7 w-7 bg-ods-border rounded animate-pulse shrink-0" }),
|
|
1602
|
-
/* @__PURE__ */ jsx8("div", { className: "h-4 w-40 bg-ods-border rounded animate-pulse" })
|
|
1603
|
-
] }) }),
|
|
1604
|
-
/* @__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: [
|
|
1605
|
-
/* @__PURE__ */ jsx8("div", { className: "h-4 w-72 bg-ods-border rounded animate-pulse" }),
|
|
1606
|
-
/* @__PURE__ */ jsx8("div", { className: "h-12 w-32 bg-ods-border rounded animate-pulse" })
|
|
1607
|
-
] })
|
|
1608
|
-
] })
|
|
1609
|
-
] });
|
|
1610
|
-
}
|
|
1611
|
-
function HelpCenterCreateForm({
|
|
1612
|
-
actions,
|
|
1613
|
-
sessionName,
|
|
1614
|
-
sessionEmail,
|
|
1615
|
-
supportSystemDown = false
|
|
1616
|
-
}) {
|
|
1617
|
-
const [subject, setSubject] = useState5("");
|
|
1618
|
-
const [subjectError, setSubjectError] = useState5(null);
|
|
1619
|
-
const subjectField = /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
|
|
1620
|
-
/* @__PURE__ */ jsxs7(Label, { htmlFor: "help-center-subject", children: [
|
|
1621
|
-
"Subject",
|
|
1622
|
-
/* @__PURE__ */ jsx8("span", { className: "text-ods-accent", children: "*" })
|
|
1623
|
-
] }),
|
|
1624
|
-
/* @__PURE__ */ jsx8(
|
|
1625
|
-
Input,
|
|
1626
|
-
{
|
|
1627
|
-
id: "help-center-subject",
|
|
1628
|
-
type: "text",
|
|
1629
|
-
value: subject,
|
|
1630
|
-
onChange: (e) => {
|
|
1631
|
-
setSubject(e.target.value);
|
|
1632
|
-
if (subjectError) setSubjectError(null);
|
|
1633
|
-
},
|
|
1634
|
-
placeholder: "Briefly describe what's going on",
|
|
1635
|
-
maxLength: SUBJECT_MAX_CHARS,
|
|
1636
|
-
"aria-invalid": !!subjectError,
|
|
1637
|
-
"aria-describedby": subjectError ? "help-center-subject-error" : void 0,
|
|
1638
|
-
disabled: supportSystemDown,
|
|
1639
|
-
className: "bg-ods-card border-ods-border text-ods-text-primary placeholder-ods-text-secondary px-3 h-12"
|
|
1640
|
-
}
|
|
1641
|
-
),
|
|
1642
|
-
subjectError && /* @__PURE__ */ jsx8(
|
|
1643
|
-
"span",
|
|
1644
|
-
{
|
|
1645
|
-
id: "help-center-subject-error",
|
|
1646
|
-
className: "text-ods-error text-xs font-['DM_Sans'] mt-1",
|
|
1647
|
-
children: subjectError
|
|
1648
|
-
}
|
|
1649
|
-
)
|
|
1650
|
-
] });
|
|
1651
|
-
return /* @__PURE__ */ jsx8(
|
|
1652
|
-
ContactForm,
|
|
1653
|
-
{
|
|
1654
|
-
title: "Open a new ticket",
|
|
1655
|
-
footerText: "The support team typically responds within one business day.",
|
|
1656
|
-
hideFields: ["name", "email", "companySize", "referralSource", "helpCategory"],
|
|
1657
|
-
defaultValues: {
|
|
1658
|
-
name: sessionName,
|
|
1659
|
-
email: sessionEmail,
|
|
1660
|
-
helpCategory: "Support Request"
|
|
1661
|
-
},
|
|
1662
|
-
extraTopField: subjectField,
|
|
1663
|
-
submitLabel: "Open ticket",
|
|
1664
|
-
attachmentsEnabled: true,
|
|
1665
|
-
onCustomSubmit: async (data, attachments) => {
|
|
1666
|
-
const trimmedSubject = subject.trim();
|
|
1667
|
-
if (!trimmedSubject) {
|
|
1668
|
-
setSubjectError("Subject is required");
|
|
1669
|
-
throw new Error("SUBJECT_REQUIRED");
|
|
1670
|
-
}
|
|
1671
|
-
setSubjectError(null);
|
|
1672
|
-
const ok = await actions.submitTicket({
|
|
1673
|
-
subject: trimmedSubject,
|
|
1674
|
-
content: data.message,
|
|
1675
|
-
attachments: attachments.length > 0 ? attachments : void 0
|
|
1676
|
-
});
|
|
1677
|
-
if (ok) {
|
|
1678
|
-
setSubject("");
|
|
1679
|
-
} else {
|
|
1680
|
-
throw new Error("TICKET_SUBMIT_FAILED");
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
);
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// src/components/tickets/help-center-list.tsx
|
|
1688
|
-
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1689
|
-
function HelpCenterList({ toast: toast2 = toast, backButton, title } = {}) {
|
|
1690
|
-
const identity = useChatIdentity();
|
|
1691
|
-
const searchParams = useSearchParams();
|
|
1692
|
-
const router = useRouter();
|
|
1693
|
-
const pathname = usePathname();
|
|
1694
|
-
const search = searchParams.get("search") || "";
|
|
1695
|
-
const status = searchParams.get("status") || "all";
|
|
1696
|
-
const ticketParam = searchParams.get("ticket") || "";
|
|
1697
|
-
const rawPage = Number(searchParams.get("page"));
|
|
1698
|
-
const page = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
|
|
1699
|
-
if (identity.isLoading) {
|
|
1700
|
-
return /* @__PURE__ */ jsx9(
|
|
1701
|
-
DevSectionPage,
|
|
1702
|
-
{
|
|
1703
|
-
sectionKey: "tickets",
|
|
1704
|
-
backButton,
|
|
1705
|
-
title,
|
|
1706
|
-
preControls: /* @__PURE__ */ jsx9(HelpCenterCreateFormSkeleton, {}),
|
|
1707
|
-
children: /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {})
|
|
1708
|
-
}
|
|
1709
|
-
);
|
|
1710
|
-
}
|
|
1711
|
-
if (identity.authTier === "anon" || !identity.user?.email) {
|
|
1712
|
-
return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", backButton, title, children: /* @__PURE__ */ jsx9(
|
|
1713
|
-
EmptyState,
|
|
1714
|
-
{
|
|
1715
|
-
type: "generic",
|
|
1716
|
-
title: "Sign in to manage tickets",
|
|
1717
|
-
description: "View, open, and follow up on support tickets after signing in.",
|
|
1718
|
-
showCTA: false
|
|
1719
|
-
}
|
|
1720
|
-
) });
|
|
1721
|
-
}
|
|
1722
|
-
const sessionName = [identity.user?.firstName, identity.user?.lastName].filter(Boolean).join(" ").trim() || identity.user?.email?.split("@")[0] || "Customer";
|
|
1723
|
-
const sessionEmail = identity.user.email;
|
|
1724
|
-
return /* @__PURE__ */ jsx9(
|
|
1725
|
-
HelpCenterListAuthed,
|
|
1726
|
-
{
|
|
1727
|
-
search,
|
|
1728
|
-
status,
|
|
1729
|
-
page,
|
|
1730
|
-
ticketParam,
|
|
1731
|
-
searchParams,
|
|
1732
|
-
router,
|
|
1733
|
-
pathname,
|
|
1734
|
-
toast: toast2,
|
|
1735
|
-
sessionName,
|
|
1736
|
-
sessionEmail,
|
|
1737
|
-
backButton,
|
|
1738
|
-
title
|
|
1739
|
-
}
|
|
1740
|
-
);
|
|
1741
|
-
}
|
|
1742
|
-
function HelpCenterListAuthed({
|
|
1743
|
-
search,
|
|
1744
|
-
status,
|
|
1745
|
-
page,
|
|
1746
|
-
ticketParam,
|
|
1747
|
-
searchParams,
|
|
1748
|
-
router,
|
|
1749
|
-
pathname,
|
|
1750
|
-
toast: toast2,
|
|
1751
|
-
sessionName,
|
|
1752
|
-
sessionEmail,
|
|
1753
|
-
backButton,
|
|
1754
|
-
title
|
|
1755
|
-
}) {
|
|
1756
|
-
const queryClient = useQueryClient3();
|
|
1757
|
-
const [optimisticTickets, setOptimisticTickets] = useState6([]);
|
|
1758
|
-
const [supportSystemDown, setSupportSystemDown] = useState6(false);
|
|
1759
|
-
const setOpenTicket = useCallback6(
|
|
1760
|
-
(externalId) => {
|
|
1761
|
-
const params = new URLSearchParams(searchParams.toString());
|
|
1762
|
-
if (externalId) params.set("ticket", externalId);
|
|
1763
|
-
else params.delete("ticket");
|
|
1764
|
-
const qs = params.toString();
|
|
1765
|
-
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
1766
|
-
},
|
|
1767
|
-
[searchParams, router, pathname]
|
|
1768
|
-
);
|
|
1769
|
-
const { tickets, isLoading, isFetching, error, refetch, totalPages } = useTicketsList({
|
|
1770
|
-
// `sessionEmail` is drilled in from the parent — see the same
|
|
1771
|
-
// pattern + race-cause rationale documented in
|
|
1772
|
-
// `HelpCenterCreateForm.sessionName/sessionEmail`. Calling
|
|
1773
|
-
// `useChatIdentity` inside `useTicketsList` would race the
|
|
1774
|
-
// parent's already-resolved identity and produce an empty-state
|
|
1775
|
-
// flash on first render.
|
|
1776
|
-
customerEmail: sessionEmail,
|
|
1777
|
-
search,
|
|
1778
|
-
status,
|
|
1779
|
-
page,
|
|
1780
|
-
// Live status: while a drawer is open, poll so an out-of-band HubSpot
|
|
1781
|
-
// status change (e.g. agent closes the ticket) flips the badge +
|
|
1782
|
-
// open/reopen affordance within one interval. Idle (no drawer) → no poll.
|
|
1783
|
-
// `ticketParam` (the open ticket's external_id) is the open signal.
|
|
1784
|
-
refetchInterval: ticketParam ? TICKET_LIVE_POLL_MS : false
|
|
1785
|
-
});
|
|
1786
|
-
const expandedTicketId = ticketParam && tickets.find((t) => t.external_id === ticketParam)?.id || null;
|
|
1787
|
-
const prependOptimistic = useCallback6((placeholder) => {
|
|
1788
|
-
setOptimisticTickets((prev) => [placeholder, ...prev]);
|
|
1789
|
-
}, []);
|
|
1790
|
-
const removeOptimistic = useCallback6((placeholderId) => {
|
|
1791
|
-
setOptimisticTickets((prev) => prev.filter((t) => t.id !== placeholderId));
|
|
1792
|
-
}, []);
|
|
1793
|
-
const removeTicketFromCache = useCallback6(
|
|
1794
|
-
(ticketId) => {
|
|
1795
|
-
queryClient.setQueriesData(
|
|
1796
|
-
{ queryKey: ["tickets"] },
|
|
1797
|
-
(prev) => {
|
|
1798
|
-
if (!prev || !Array.isArray(prev.tickets)) return prev;
|
|
1799
|
-
const nextTickets = prev.tickets.filter((t) => t.id !== ticketId);
|
|
1800
|
-
if (nextTickets.length === prev.tickets.length) return prev;
|
|
1801
|
-
return { ...prev, tickets: nextTickets };
|
|
1802
|
-
}
|
|
1803
|
-
);
|
|
1804
|
-
},
|
|
1805
|
-
[queryClient]
|
|
1806
|
-
);
|
|
1807
|
-
const actions = useTicketActions({
|
|
1808
|
-
prependOptimistic,
|
|
1809
|
-
removeOptimistic,
|
|
1810
|
-
removeTicketFromCache,
|
|
1811
|
-
toast: toast2,
|
|
1812
|
-
onSupportSystemDown: () => setSupportSystemDown(true)
|
|
1813
|
-
});
|
|
1814
|
-
const toggleRow = useCallback6(
|
|
1815
|
-
(id) => {
|
|
1816
|
-
const t = tickets.find((x) => x.id === id);
|
|
1817
|
-
if (!t?.external_id) return;
|
|
1818
|
-
setOpenTicket(t.external_id === ticketParam ? null : t.external_id);
|
|
1819
|
-
},
|
|
1820
|
-
[tickets, ticketParam, setOpenTicket]
|
|
1821
|
-
);
|
|
1822
|
-
const merged = [...optimisticTickets, ...tickets];
|
|
1823
|
-
useScrollToHash(tickets, { headerOffset: STICKY_HEADER_OFFSET_PX });
|
|
1824
|
-
const hasActiveFilters = search !== "" || status !== "" && status !== "all";
|
|
1825
|
-
const hasResults = merged.length > 0;
|
|
1826
|
-
const form = /* @__PURE__ */ jsx9(
|
|
1827
|
-
HelpCenterCreateForm,
|
|
1828
|
-
{
|
|
1829
|
-
actions,
|
|
1830
|
-
sessionName,
|
|
1831
|
-
sessionEmail,
|
|
1832
|
-
supportSystemDown
|
|
1833
|
-
}
|
|
1834
|
-
);
|
|
1835
|
-
const body = /* @__PURE__ */ jsxs8("div", { className: "w-full flex flex-col gap-[40px]", children: [
|
|
1836
|
-
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: [
|
|
1837
|
-
/* @__PURE__ */ jsxs8("p", { className: "text-ods-error text-base", children: [
|
|
1838
|
-
"Couldn\u2019t load your tickets. ",
|
|
1839
|
-
error.message
|
|
1840
|
-
] }),
|
|
1841
|
-
/* @__PURE__ */ jsx9(Button, { type: "button", variant: "accent", onClick: () => refetch(), children: "Retry" })
|
|
1842
|
-
] }),
|
|
1843
|
-
!error && /* @__PURE__ */ jsx9("div", { className: "w-full", children: isLoading ? /* @__PURE__ */ jsx9(DevCardRowSkeletonList, {}) : !hasResults && isFetching ? (
|
|
1844
|
-
// Bridge state — background refetch in flight and the
|
|
1845
|
-
// optimistic placeholder was just removed by the mutation
|
|
1846
|
-
// callback. Without this branch "No tickets yet" would flash
|
|
1847
|
-
// for ~50ms between `removeOptimistic` and the server
|
|
1848
|
-
// response landing.
|
|
1849
|
-
/* @__PURE__ */ jsx9(DevCardRowSkeletonList, { rows: 1 })
|
|
1850
|
-
) : !hasResults ? hasActiveFilters ? /* @__PURE__ */ jsx9(
|
|
1851
|
-
EmptyState,
|
|
1852
|
-
{
|
|
1853
|
-
type: "search",
|
|
1854
|
-
title: "No tickets found",
|
|
1855
|
-
description: "No tickets match your current filters. Try clearing them or broadening your search.",
|
|
1856
|
-
showCTA: true,
|
|
1857
|
-
ctaText: "Reset filters",
|
|
1858
|
-
onCtaClick: () => {
|
|
1859
|
-
const params = new URLSearchParams(searchParams.toString());
|
|
1860
|
-
params.delete("search");
|
|
1861
|
-
params.delete("status");
|
|
1862
|
-
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
) : /* @__PURE__ */ jsx9(
|
|
1866
|
-
EmptyState,
|
|
1867
|
-
{
|
|
1868
|
-
type: "generic",
|
|
1869
|
-
title: "No tickets yet",
|
|
1870
|
-
description: "Open one above to start the conversation with the support team.",
|
|
1871
|
-
showCTA: false
|
|
1872
|
-
}
|
|
1873
|
-
) : (
|
|
1874
|
-
// `overflow-clip` (NOT `overflow-hidden`) — both visually
|
|
1875
|
-
// clip the rounded corners, but `hidden` makes the element
|
|
1876
|
-
// a "scroll container" per CSSOM spec, which causes
|
|
1877
|
-
// `scrollIntoView` calls inside (`<HelpCenterCard>` click
|
|
1878
|
-
// handlers) to try scrolling THIS div (can't, overflow
|
|
1879
|
-
// hidden) instead of bubbling up to the window. `clip`
|
|
1880
|
-
// keeps the visual clip but NOT the scroll-container
|
|
1881
|
-
// status, so click-to-scroll actually moves the page.
|
|
1882
|
-
/* @__PURE__ */ jsx9("div", { className: "bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full", children: merged.map((ticket) => /* @__PURE__ */ jsx9(
|
|
1883
|
-
HelpCenterCard,
|
|
1884
|
-
{
|
|
1885
|
-
id: devSectionAnchorId("ticket", ticket.external_id),
|
|
1886
|
-
ticket,
|
|
1887
|
-
expanded: expandedTicketId === ticket.id,
|
|
1888
|
-
onToggle: toggleRow,
|
|
1889
|
-
busy: isOptimistic(ticket) ? false : actions.isRowBusy(ticket.id),
|
|
1890
|
-
supportSystemDown,
|
|
1891
|
-
onSendMessage: actions.sendMessage,
|
|
1892
|
-
onClose: actions.closeTicket,
|
|
1893
|
-
onReopen: actions.reopenTicket,
|
|
1894
|
-
onActionCollapsed: () => setOpenTicket(null),
|
|
1895
|
-
replyError: actions.replyErrorFor(ticket.external_id),
|
|
1896
|
-
onClearReplyError: () => actions.clearReplyError(ticket.external_id)
|
|
1897
|
-
},
|
|
1898
|
-
ticket.id
|
|
1899
|
-
)) })
|
|
1900
|
-
) }),
|
|
1901
|
-
!error && totalPages > 1 && /* @__PURE__ */ jsx9(UnifiedPagination, { currentPage: page, totalPages })
|
|
1902
|
-
] });
|
|
1903
|
-
return /* @__PURE__ */ jsx9(DevSectionPage, { sectionKey: "tickets", backButton, title, preControls: form, children: body });
|
|
1904
|
-
}
|
|
1905
38
|
export {
|
|
1906
39
|
HelpCenterCard,
|
|
1907
40
|
HelpCenterCreateForm,
|