@flamingo-stack/openframe-frontend-core 0.0.503 → 0.0.504-snapshot.20260801181709
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-SGWPW2AO.cjs → chunk-2A74VRAF.cjs} +18 -18
- package/dist/{chunk-SGWPW2AO.cjs.map → chunk-2A74VRAF.cjs.map} +1 -1
- package/dist/{chunk-LILQJ3UZ.cjs → chunk-3ENGUBJP.cjs} +1 -1
- package/dist/chunk-3ENGUBJP.cjs.map +1 -0
- package/dist/{chunk-AWDHRIL3.cjs → chunk-4ST2BOWJ.cjs} +75 -75
- package/dist/{chunk-AWDHRIL3.cjs.map → chunk-4ST2BOWJ.cjs.map} +1 -1
- package/dist/{chunk-SFJRMBJC.js → chunk-5SVNA7IU.js} +2 -2
- package/dist/{chunk-OAWFYZZT.js → chunk-7DOSAJDK.js} +3 -3
- package/dist/{chunk-J4T5ZBWG.cjs → chunk-BZRP54IO.cjs} +19 -19
- package/dist/{chunk-J4T5ZBWG.cjs.map → chunk-BZRP54IO.cjs.map} +1 -1
- package/dist/{chunk-BZF63JXP.js → chunk-CRJVO4KO.js} +6 -6
- package/dist/{chunk-5RIHLXEP.cjs → chunk-DDVQ5NKK.cjs} +33 -33
- package/dist/{chunk-5RIHLXEP.cjs.map → chunk-DDVQ5NKK.cjs.map} +1 -1
- package/dist/{chunk-HR6F6IP4.js → chunk-DIKJLAMY.js} +78 -43
- package/dist/chunk-DIKJLAMY.js.map +1 -0
- package/dist/{chunk-VO2ZONMR.js → chunk-FNTFWP2A.js} +3 -3
- package/dist/{chunk-2OXJ6NHI.cjs → chunk-GJZ42TTG.cjs} +7 -7
- package/dist/{chunk-2OXJ6NHI.cjs.map → chunk-GJZ42TTG.cjs.map} +1 -1
- package/dist/{chunk-JIZJDAXI.cjs → chunk-GXRVBKYX.cjs} +12 -12
- package/dist/{chunk-JIZJDAXI.cjs.map → chunk-GXRVBKYX.cjs.map} +1 -1
- package/dist/{chunk-F7AD5CUU.js → chunk-H3N4P3CG.js} +3 -3
- package/dist/chunk-H3N4P3CG.js.map +1 -0
- package/dist/{chunk-VCPTU4IW.js → chunk-HWER6KGU.js} +7 -7
- package/dist/{chunk-4SR2QHMB.js → chunk-ITVWX5HX.js} +3 -3
- package/dist/{chunk-FFXNRJ5S.cjs → chunk-LOBR7IC6.cjs} +6 -6
- package/dist/chunk-LOBR7IC6.cjs.map +1 -0
- package/dist/{chunk-72DF3QTA.cjs → chunk-MJQ263KS.cjs} +14 -14
- package/dist/{chunk-72DF3QTA.cjs.map → chunk-MJQ263KS.cjs.map} +1 -1
- package/dist/{chunk-FTOVZ7O2.js → chunk-NPO6CHCR.js} +7 -7
- package/dist/{chunk-XV5SXALY.js → chunk-NT5FX7GZ.js} +3 -3
- package/dist/{chunk-UCV4LFUG.cjs → chunk-NY75FKN6.cjs} +40 -40
- package/dist/{chunk-UCV4LFUG.cjs.map → chunk-NY75FKN6.cjs.map} +1 -1
- package/dist/{chunk-4UNQMYP7.cjs → chunk-O32Y4E4R.cjs} +3 -3
- package/dist/{chunk-4UNQMYP7.cjs.map → chunk-O32Y4E4R.cjs.map} +1 -1
- package/dist/{chunk-CLQEBWV7.js → chunk-QE55ROZW.js} +4 -4
- package/dist/{chunk-WHJGRRK2.js → chunk-QW7SW5HY.js} +52 -43
- package/dist/chunk-QW7SW5HY.js.map +1 -0
- package/dist/{chunk-JGXAVNZQ.js → chunk-R6CKL7IR.js} +2464 -1886
- package/dist/chunk-R6CKL7IR.js.map +1 -0
- package/dist/{chunk-L5CI57RM.cjs → chunk-T3BF6JTQ.cjs} +52 -43
- package/dist/chunk-T3BF6JTQ.cjs.map +1 -0
- package/dist/{chunk-G43CEISB.js → chunk-TA3VJ2QO.js} +4 -4
- package/dist/{chunk-7L2RVINX.cjs → chunk-TRS4CIAS.cjs} +35 -35
- package/dist/{chunk-7L2RVINX.cjs.map → chunk-TRS4CIAS.cjs.map} +1 -1
- package/dist/{chunk-DMXB3VE2.cjs → chunk-U3YLML53.cjs} +1837 -1259
- package/dist/chunk-U3YLML53.cjs.map +1 -0
- package/dist/{chunk-3T4SMJM5.cjs → chunk-VDKNUC3N.cjs} +181 -146
- package/dist/chunk-VDKNUC3N.cjs.map +1 -0
- package/dist/{chunk-RR3Z52X5.cjs → chunk-W3QCFQ56.cjs} +42 -42
- package/dist/{chunk-RR3Z52X5.cjs.map → chunk-W3QCFQ56.cjs.map} +1 -1
- package/dist/{chunk-VYEDTVIZ.js → chunk-XJPMTJAR.js} +4 -4
- package/dist/{chunk-6GT557KN.js → chunk-XKIA4XVE.js} +1 -1
- package/dist/chunk-XKIA4XVE.js.map +1 -0
- package/dist/components/case-studies/index.cjs +11 -11
- package/dist/components/case-studies/index.js +5 -5
- package/dist/components/chat/index.cjs +5 -5
- package/dist/components/chat/index.js +4 -4
- package/dist/components/contact/index.cjs +6 -6
- package/dist/components/contact/index.js +5 -5
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +6 -6
- package/dist/components/embeds/index.js +5 -5
- package/dist/components/faq/index.cjs +7 -7
- package/dist/components/faq/index.js +6 -6
- package/dist/components/features/index.cjs +5 -5
- package/dist/components/features/index.js +4 -4
- package/dist/components/help-center-pages/index.cjs +24 -24
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +169 -165
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +17 -13
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts +8 -0
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/header-button.d.ts +5 -1
- package/dist/components/navigation/header-button.d.ts.map +1 -1
- package/dist/components/navigation/header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +9 -5
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.d.ts +4 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +8 -4
- package/dist/components/navigation/mingo-ai-button.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/ticket-alerts-button.d.ts +29 -0
- package/dist/components/navigation/ticket-alerts-button.d.ts.map +1 -0
- package/dist/components/navigation/top-navigation.d.ts +5 -1
- package/dist/components/navigation/top-navigation.d.ts.map +1 -1
- package/dist/components/navigation/unread-dot.d.ts +19 -0
- package/dist/components/navigation/unread-dot.d.ts.map +1 -0
- package/dist/components/onboarding-guides/index.cjs +9 -9
- package/dist/components/onboarding-guides/index.js +8 -8
- package/dist/components/related-content/index.cjs +7 -7
- package/dist/components/related-content/index.js +6 -6
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-ticket-actions.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-ticket-engagements.d.ts +1 -9
- package/dist/components/tickets/hooks/use-ticket-engagements.d.ts.map +1 -1
- package/dist/components/tickets/hooks/use-tickets-list.d.ts +0 -7
- package/dist/components/tickets/hooks/use-tickets-list.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +15 -8
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.d.ts +2 -1
- package/dist/components/tickets/index.d.ts.map +1 -1
- package/dist/components/tickets/index.js +14 -7
- package/dist/components/tickets/ticket-detail-drawer.d.ts.map +1 -1
- package/dist/components/tickets/ticket-live-provider.d.ts +58 -0
- package/dist/components/tickets/ticket-live-provider.d.ts.map +1 -0
- package/dist/components/tickets/types.d.ts +58 -11
- package/dist/components/tickets/types.d.ts.map +1 -1
- package/dist/components/ui/button/button.d.ts +6 -5
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +5 -5
- package/dist/components/ui/index.js +4 -4
- package/dist/contexts/chat-runtime-context.d.ts +8 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/hooks/ui/use-header-height.d.ts.map +1 -1
- package/dist/index.cjs +11 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -4
- package/dist/types/navigation.d.ts +10 -0
- package/dist/types/navigation.d.ts.map +1 -1
- package/dist/utils/index.cjs +171 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +171 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/sse-subscription.d.ts +60 -0
- package/dist/utils/sse-subscription.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/features/time-tracker/time-tracker-header-button.tsx +1 -1
- package/src/components/navigation/app-header.tsx +36 -20
- package/src/components/navigation/header-button.tsx +18 -2
- package/src/components/navigation/header-mingo-button.tsx +2 -2
- package/src/components/navigation/header-skeleton.tsx +4 -4
- package/src/components/navigation/header.tsx +41 -21
- package/src/components/navigation/index.ts +4 -0
- package/src/components/navigation/mingo-ai-button.tsx +23 -22
- package/src/components/navigation/mobile-burger-menu.tsx +3 -3
- package/src/components/navigation/navigation-sidebar-item.tsx +2 -3
- package/src/components/navigation/ticket-alerts-button.tsx +61 -0
- package/src/components/navigation/top-navigation.tsx +17 -1
- package/src/components/navigation/unread-dot.tsx +34 -0
- package/src/components/tickets/.types.md +1 -2
- package/src/components/tickets/help-center-card.tsx +16 -0
- package/src/components/tickets/help-center-list.tsx +6 -6
- package/src/components/tickets/hooks/.use-ticket-engagements.md +3 -12
- package/src/components/tickets/hooks/.use-tickets-list.md +0 -2
- package/src/components/tickets/hooks/use-ticket-actions.ts +9 -0
- package/src/components/tickets/hooks/use-ticket-engagements.ts +4 -13
- package/src/components/tickets/hooks/use-tickets-list.ts +2 -13
- package/src/components/tickets/index.ts +10 -0
- package/src/components/tickets/ticket-detail-drawer.tsx +45 -13
- package/src/components/tickets/ticket-live-provider.tsx +474 -0
- package/src/components/tickets/types.ts +62 -11
- package/src/components/ui/button/button.tsx +165 -133
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/src/hooks/ui/use-header-height.ts +3 -1
- package/src/stories/TopNavigation.stories.tsx +20 -1
- package/src/types/navigation.ts +10 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/sse-subscription.ts +276 -0
- package/dist/chunk-3T4SMJM5.cjs.map +0 -1
- package/dist/chunk-6GT557KN.js.map +0 -1
- package/dist/chunk-DMXB3VE2.cjs.map +0 -1
- package/dist/chunk-F7AD5CUU.js.map +0 -1
- package/dist/chunk-FFXNRJ5S.cjs.map +0 -1
- package/dist/chunk-HR6F6IP4.js.map +0 -1
- package/dist/chunk-JGXAVNZQ.js.map +0 -1
- package/dist/chunk-L5CI57RM.cjs.map +0 -1
- package/dist/chunk-LILQJ3UZ.cjs.map +0 -1
- package/dist/chunk-WHJGRRK2.js.map +0 -1
- /package/dist/{chunk-SFJRMBJC.js.map → chunk-5SVNA7IU.js.map} +0 -0
- /package/dist/{chunk-OAWFYZZT.js.map → chunk-7DOSAJDK.js.map} +0 -0
- /package/dist/{chunk-BZF63JXP.js.map → chunk-CRJVO4KO.js.map} +0 -0
- /package/dist/{chunk-VO2ZONMR.js.map → chunk-FNTFWP2A.js.map} +0 -0
- /package/dist/{chunk-VCPTU4IW.js.map → chunk-HWER6KGU.js.map} +0 -0
- /package/dist/{chunk-4SR2QHMB.js.map → chunk-ITVWX5HX.js.map} +0 -0
- /package/dist/{chunk-FTOVZ7O2.js.map → chunk-NPO6CHCR.js.map} +0 -0
- /package/dist/{chunk-XV5SXALY.js.map → chunk-NT5FX7GZ.js.map} +0 -0
- /package/dist/{chunk-CLQEBWV7.js.map → chunk-QE55ROZW.js.map} +0 -0
- /package/dist/{chunk-G43CEISB.js.map → chunk-TA3VJ2QO.js.map} +0 -0
- /package/dist/{chunk-VYEDTVIZ.js.map → chunk-XJPMTJAR.js.map} +0 -0
|
@@ -5,6 +5,17 @@ import { cn } from '../../utils/cn';
|
|
|
5
5
|
|
|
6
6
|
export type TopNavigationCenterBreakpoint = 'md' | 'lg';
|
|
7
7
|
|
|
8
|
+
export type TopNavigationSize = 'small' | 'big';
|
|
9
|
+
|
|
10
|
+
// Bar height + square-cell width per size (Figma 2797-5978 `size` axis).
|
|
11
|
+
// Heights are FIXED across breakpoints. `--top-nav-cell` is consumed by
|
|
12
|
+
// `HeaderButton` (and any custom cell) so cell widths follow the size without
|
|
13
|
+
// each cell knowing about it.
|
|
14
|
+
const SIZE_CLASSES: Record<TopNavigationSize, string> = {
|
|
15
|
+
small: 'h-14 [--top-nav-cell:3.5rem]',
|
|
16
|
+
big: 'h-[72px] [--top-nav-cell:4.5rem]',
|
|
17
|
+
};
|
|
18
|
+
|
|
8
19
|
// Literal class maps — Tailwind's scanner needs the full class strings.
|
|
9
20
|
const CENTER_VISIBILITY: Record<TopNavigationCenterBreakpoint, string> = {
|
|
10
21
|
md: 'hidden md:flex',
|
|
@@ -52,6 +63,9 @@ export interface TopNavigationProps extends React.HTMLAttributes<HTMLElement> {
|
|
|
52
63
|
backgroundClassName?: string;
|
|
53
64
|
/** Top border on mobile (the ODS spec bar shows it below md). Default true. */
|
|
54
65
|
mobileTopBorder?: boolean;
|
|
66
|
+
/** Bar size (Figma 2797-5978 `size` axis): `small` = 56px (console),
|
|
67
|
+
* `big` = 72px (marketing/hub sites). Fixed across breakpoints. */
|
|
68
|
+
size?: TopNavigationSize;
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
const hasContent = (node: React.ReactNode): boolean => node !== null && node !== undefined && node !== false;
|
|
@@ -80,6 +94,7 @@ export function TopNavigation({
|
|
|
80
94
|
sideActions,
|
|
81
95
|
backgroundClassName,
|
|
82
96
|
mobileTopBorder = true,
|
|
97
|
+
size = 'small',
|
|
83
98
|
className,
|
|
84
99
|
children,
|
|
85
100
|
...props
|
|
@@ -87,7 +102,8 @@ export function TopNavigation({
|
|
|
87
102
|
return (
|
|
88
103
|
<header
|
|
89
104
|
className={cn(
|
|
90
|
-
'flex
|
|
105
|
+
'flex w-full items-center border-b border-ods-border',
|
|
106
|
+
SIZE_CLASSES[size],
|
|
91
107
|
mobileTopBorder && 'border-t md:border-t-0',
|
|
92
108
|
backgroundClassName ?? 'bg-ods-card',
|
|
93
109
|
className,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { cn } from '../../utils/cn'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The ONE unread-indicator dot (`bg-ods-warning`, top-right of an icon
|
|
8
|
+
* inside a `relative` wrapper). Previously duplicated in
|
|
9
|
+
* `NotificationsHeaderButton` and `NavigationSidebarItemButton` — both
|
|
10
|
+
* now render THIS, as does `TicketAlertsButton`.
|
|
11
|
+
*
|
|
12
|
+
* Size variants preserve the two pre-existing renders exactly (neither
|
|
13
|
+
* may silently change):
|
|
14
|
+
* - `responsive` — header cells: `w-1.5 h-1.5 md:w-2 md:h-2`
|
|
15
|
+
* - `fixed` — sidebar collapsed icon: `w-2 h-2`
|
|
16
|
+
*/
|
|
17
|
+
export interface UnreadDotProps {
|
|
18
|
+
size?: 'responsive' | 'fixed'
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function UnreadDot({ size = 'responsive', className }: UnreadDotProps) {
|
|
23
|
+
return (
|
|
24
|
+
<span
|
|
25
|
+
className={cn(
|
|
26
|
+
'absolute top-0 right-0 bg-ods-warning rounded-full',
|
|
27
|
+
size === 'responsive' ? 'w-1.5 h-1.5 md:w-2 md:h-2' : 'w-2 h-2',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default UnreadDot
|
|
@@ -28,7 +28,7 @@ Shared type contracts, constants, and utilities for the OpenFrame ticket system
|
|
|
28
28
|
| Name | Value | Purpose |
|
|
29
29
|
|---|---|---|
|
|
30
30
|
| `TICKET_TEXT_MAX_CHARS` | `5000` | Client-side cap on ticket/comment text length |
|
|
31
|
-
| `
|
|
31
|
+
| `TICKET_MARK_READ_DEBOUNCE_MS` | `2000` | Trailing debounce for drawer mark-read while new messages stream in |
|
|
32
32
|
| `TOAST_COPY` | `as const` object | Centralized, QA-auditable toast strings for all ticket actions |
|
|
33
33
|
|
|
34
34
|
## Usage Example
|
|
@@ -38,7 +38,6 @@ import {
|
|
|
38
38
|
AnyTicket,
|
|
39
39
|
isOptimistic,
|
|
40
40
|
TicketsCacheSlot,
|
|
41
|
-
TICKET_LIVE_POLL_MS,
|
|
42
41
|
TICKET_TEXT_MAX_CHARS,
|
|
43
42
|
TOAST_COPY,
|
|
44
43
|
} from '@openframe/oss-lib'
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
TicketDetailDrawer,
|
|
27
27
|
type TicketDetailDrawerProps,
|
|
28
28
|
} from './ticket-detail-drawer'
|
|
29
|
+
import { useOptionalTicketLive } from './ticket-live-provider'
|
|
29
30
|
import type { AnyTicket } from './types'
|
|
30
31
|
import { isOptimistic } from './types'
|
|
31
32
|
|
|
@@ -118,8 +119,23 @@ export function HelpCenterCard({
|
|
|
118
119
|
return () => cancelAnimationFrame(raf)
|
|
119
120
|
}, [isExpanded])
|
|
120
121
|
|
|
122
|
+
// Unread replies for this row — read from the provider's single summary
|
|
123
|
+
// map (missing key / no provider = 0). The provider masks the open
|
|
124
|
+
// ticket to 0 and zeroes on markRead, so no local state here.
|
|
125
|
+
const live = useOptionalTicketLive()
|
|
126
|
+
const unreadCount =
|
|
127
|
+
(!optimistic && ticket.external_id && live?.unreadByTicket[ticket.external_id]) || 0
|
|
128
|
+
|
|
121
129
|
const rightBadges = (
|
|
122
130
|
<>
|
|
131
|
+
{unreadCount > 0 && (
|
|
132
|
+
<StatusBadge
|
|
133
|
+
text={`${unreadCount} NEW`}
|
|
134
|
+
colorScheme="warning"
|
|
135
|
+
variant="card"
|
|
136
|
+
className="border border-ods-border"
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
123
139
|
<StatusBadge
|
|
124
140
|
text={rawStatus}
|
|
125
141
|
colorScheme={getStatusColorScheme(rawStatus)}
|
|
@@ -44,7 +44,7 @@ import { useTicketActions } from './hooks/use-ticket-actions'
|
|
|
44
44
|
import { HelpCenterCard } from './help-center-card'
|
|
45
45
|
import { HelpCenterCreateForm, HelpCenterCreateFormSkeleton } from './help-center-create-form'
|
|
46
46
|
import type { AnyTicket, OptimisticTicket, TicketsCacheSlot } from './types'
|
|
47
|
-
import { isOptimistic
|
|
47
|
+
import { isOptimistic } from './types'
|
|
48
48
|
|
|
49
49
|
export interface HelpCenterListProps {
|
|
50
50
|
/** Toast override (test-friendly). Defaults to the lib's shared
|
|
@@ -212,11 +212,11 @@ function HelpCenterListAuthed({
|
|
|
212
212
|
search,
|
|
213
213
|
status,
|
|
214
214
|
page,
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
215
|
+
// No interval polling (deleted 2026-08): `TicketLiveProvider`
|
|
216
|
+
// invalidates `['tickets']` on stream events; focus/mount refetch
|
|
217
|
+
// covers hosts without a stream. Disclosed trade-off: a bare
|
|
218
|
+
// status/pipeline change with no accompanying reply has no live
|
|
219
|
+
// path until the next event/focus/reconnect.
|
|
220
220
|
})
|
|
221
221
|
|
|
222
222
|
// Open state DERIVED from the URL param. `?ticket=` carries the user-facing
|
|
@@ -11,28 +11,19 @@ Fetches and manages the conversation timeline (Note engagements and attachments)
|
|
|
11
11
|
|
|
12
12
|
### Hook
|
|
13
13
|
|
|
14
|
-
**`useTicketEngagements(externalTicketId, enabled
|
|
14
|
+
**`useTicketEngagements(externalTicketId, enabled?)`**
|
|
15
15
|
|
|
16
16
|
- Skips fetching for optimistic (`temp-*`) ticket IDs and unauthenticated (`anon`) identities
|
|
17
17
|
- Posts to `/api/chat/agent/list-engagements` via `embedAuthedFetch`
|
|
18
18
|
- Disables all caching (`staleTime: 0`, `gcTime: 0`) to ensure HubSpot truth on every drawer open
|
|
19
|
-
-
|
|
19
|
+
- No interval polling — live refresh is push-driven (`TicketLiveProvider` invalidates `['ticket-engagements', id]` on stream events); focus/mount refetch is the no-stream fallback
|
|
20
20
|
- Fixes a skeleton double-flash by treating a fetchable ticket as "loading" from the very first render, even before `useChatIdentity` resolves
|
|
21
21
|
|
|
22
22
|
## Usage Example
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
//
|
|
25
|
+
// Fetch when the drawer opens; live updates arrive via TicketLiveProvider
|
|
26
26
|
const { engagements, isLoading, error } = useTicketEngagements(ticket.externalId)
|
|
27
|
-
|
|
28
|
-
// Live-polling variant — refresh every 15 seconds while drawer is open
|
|
29
|
-
const TICKET_LIVE_POLL_MS = 15_000
|
|
30
|
-
|
|
31
|
-
const { engagements, isFetching, refetch } = useTicketEngagements(
|
|
32
|
-
ticket.externalId,
|
|
33
|
-
true,
|
|
34
|
-
TICKET_LIVE_POLL_MS,
|
|
35
|
-
)
|
|
36
27
|
```
|
|
37
28
|
|
|
38
29
|
## Notes
|
|
@@ -9,7 +9,6 @@ Input interface controlling the query:
|
|
|
9
9
|
- `search` — free-text FTS filter; empty string returns all tickets
|
|
10
10
|
- `status` — `'open'` | `'closed'` | `'all'` (empty/`'all'` omits the filter)
|
|
11
11
|
- `page` / `pageSize` — 1-based pagination; server caps `pageSize` at 100
|
|
12
|
-
- `refetchInterval` — optional polling interval (ms) for live status updates; paused on hidden tabs
|
|
13
12
|
|
|
14
13
|
### `UseTicketsListReturn`
|
|
15
14
|
Output shape:
|
|
@@ -41,7 +40,6 @@ const {
|
|
|
41
40
|
status: 'open',
|
|
42
41
|
page: currentPage,
|
|
43
42
|
pageSize: 20,
|
|
44
|
-
refetchInterval: isDrawerOpen ? 10_000 : false,
|
|
45
43
|
})
|
|
46
44
|
```
|
|
47
45
|
|
|
@@ -33,6 +33,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
33
33
|
import { useQueryClient } from '@tanstack/react-query'
|
|
34
34
|
import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context'
|
|
35
35
|
import { embedAuthedFetch } from '../../../utils/embed-authed-fetch'
|
|
36
|
+
import { useOptionalTicketLive } from '../ticket-live-provider'
|
|
36
37
|
import type { ChatAttachment } from '../../chat/utils/chat-attachment-markdown'
|
|
37
38
|
import {
|
|
38
39
|
type AnyTicket,
|
|
@@ -154,6 +155,10 @@ export function useTicketActions(options: UseTicketActionsOptions): UseTicketAct
|
|
|
154
155
|
// the bare hub path when unconfigured.
|
|
155
156
|
const ticketActionEndpoint =
|
|
156
157
|
useRequiredChatRuntime().endpoints.ticketActionUrl ?? TICKET_ACTION_ENDPOINT
|
|
158
|
+
// Optional live-stream context: a first-ever ticket must retry the
|
|
159
|
+
// provider's `no-stream` (204) subscription so replies stream in.
|
|
160
|
+
const ticketLive = useOptionalTicketLive()
|
|
161
|
+
const notifyTicketCreated = ticketLive?.notifyTicketCreated
|
|
157
162
|
const { prependOptimistic, removeOptimistic, removeTicketFromCache, toast, onSupportSystemDown } = options
|
|
158
163
|
|
|
159
164
|
// Form-level single-flight uses BOTH a ref (for synchronous guarding
|
|
@@ -390,6 +395,9 @@ export function useTicketActions(options: UseTicketActionsOptions): UseTicketAct
|
|
|
390
395
|
await queryClient.invalidateQueries({ queryKey: ['tickets'] })
|
|
391
396
|
removeOptimistic(placeholderId)
|
|
392
397
|
}
|
|
398
|
+
// A zero-ticket viewer's stream returned 204 before this
|
|
399
|
+
// create — nudge the provider to (re)subscribe.
|
|
400
|
+
notifyTicketCreated?.()
|
|
393
401
|
return true
|
|
394
402
|
})
|
|
395
403
|
} catch (err) {
|
|
@@ -409,6 +417,7 @@ export function useTicketActions(options: UseTicketActionsOptions): UseTicketAct
|
|
|
409
417
|
queryClient,
|
|
410
418
|
toast,
|
|
411
419
|
watchMirrorSync,
|
|
420
|
+
notifyTicketCreated,
|
|
412
421
|
surfaceError,
|
|
413
422
|
],
|
|
414
423
|
)
|
|
@@ -77,14 +77,6 @@ export interface UseTicketEngagementsReturn {
|
|
|
77
77
|
export function useTicketEngagements(
|
|
78
78
|
externalTicketId: string | null | undefined,
|
|
79
79
|
enabled = true,
|
|
80
|
-
/** Poll cadence (ms) for live conversation refresh while the drawer is
|
|
81
|
-
* open. The drawer only mounts this hook when expanded, so a constant
|
|
82
|
-
* here is already gated to "drawer open" — closing the drawer unmounts
|
|
83
|
-
* the panel and the polling stops. `false`/undefined disables it (the
|
|
84
|
-
* default, preserving prior fetch-once-per-open behavior for any other
|
|
85
|
-
* caller). Mirrors `useTicketsList.refetchInterval`; see
|
|
86
|
-
* `TICKET_LIVE_POLL_MS`. */
|
|
87
|
-
refetchInterval: number | false = false,
|
|
88
80
|
): UseTicketEngagementsReturn {
|
|
89
81
|
const identity = useChatIdentity()
|
|
90
82
|
const identityKey = identity.user?.email ?? 'anon'
|
|
@@ -115,11 +107,10 @@ export function useTicketEngagements(
|
|
|
115
107
|
gcTime: 0,
|
|
116
108
|
refetchOnMount: 'always',
|
|
117
109
|
refetchOnWindowFocus: true,
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
refetchInterval,
|
|
110
|
+
// NO interval polling (deleted 2026-08): new agent replies arrive via
|
|
111
|
+
// `TicketLiveProvider`'s stream-driven invalidation of
|
|
112
|
+
// `['ticket-engagements', id]`; focus/mount refetch is the no-stream
|
|
113
|
+
// fallback.
|
|
123
114
|
queryFn: async (): Promise<TicketEngagement[]> => {
|
|
124
115
|
const response = await embedAuthedFetch(listEngagementsEndpoint, {
|
|
125
116
|
method: 'POST',
|
|
@@ -59,13 +59,6 @@ export interface UseTicketsListFilters {
|
|
|
59
59
|
page?: number
|
|
60
60
|
/** Items per page (server caps at 100). Defaults to 20. */
|
|
61
61
|
pageSize?: number
|
|
62
|
-
/** Poll interval (ms) for live updates — e.g. so a ticket CLOSED out-of-band
|
|
63
|
-
* on HubSpot flips the status badge + open/reopen affordance without a manual
|
|
64
|
-
* refresh. `false`/0/undefined disables polling. The hub passes a value only
|
|
65
|
-
* while a drawer is open (mirror webhooks keep the server fresh; this surfaces
|
|
66
|
-
* it client-side). TanStack pauses interval polling while the tab is hidden,
|
|
67
|
-
* so there are no wasted background requests. */
|
|
68
|
-
refetchInterval?: number | false
|
|
69
62
|
}
|
|
70
63
|
|
|
71
64
|
export interface UseTicketsListReturn {
|
|
@@ -116,8 +109,6 @@ export function useTicketsList(filters: UseTicketsListFilters): UseTicketsListRe
|
|
|
116
109
|
// previous identity's data.
|
|
117
110
|
const identityKey = customerEmail || 'anon'
|
|
118
111
|
|
|
119
|
-
const refetchInterval = filters.refetchInterval ?? false
|
|
120
|
-
|
|
121
112
|
const query = useQuery({
|
|
122
113
|
queryKey: ['tickets', 'self', identityKey, search, statusFilter, page, pageSize],
|
|
123
114
|
enabled,
|
|
@@ -130,10 +121,8 @@ export function useTicketsList(filters: UseTicketsListFilters): UseTicketsListRe
|
|
|
130
121
|
gcTime: 0,
|
|
131
122
|
refetchOnMount: 'always',
|
|
132
123
|
refetchOnWindowFocus: true,
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// pauses on a hidden tab — no wasted requests when the user tabs away.
|
|
136
|
-
refetchInterval,
|
|
124
|
+
// NO interval polling (deleted 2026-08): live freshness is push-driven
|
|
125
|
+
// — `TicketLiveProvider` invalidates `['tickets']` on stream events.
|
|
137
126
|
queryFn: async (): Promise<FindTicketResponse> => {
|
|
138
127
|
const body: Record<string, string | number> = {
|
|
139
128
|
query: search,
|
|
@@ -38,6 +38,12 @@ export {
|
|
|
38
38
|
type TicketEngagement,
|
|
39
39
|
type TicketEngagementFile,
|
|
40
40
|
} from './hooks/use-ticket-engagements'
|
|
41
|
+
export {
|
|
42
|
+
TicketLiveProvider,
|
|
43
|
+
useTicketLive,
|
|
44
|
+
useOptionalTicketLive,
|
|
45
|
+
type TicketLiveContextValue,
|
|
46
|
+
} from './ticket-live-provider'
|
|
41
47
|
export {
|
|
42
48
|
type TicketData,
|
|
43
49
|
type TicketClickupSummary,
|
|
@@ -45,6 +51,10 @@ export {
|
|
|
45
51
|
type AnyTicket,
|
|
46
52
|
type TicketActionErrorCode,
|
|
47
53
|
type MappedTicketActionError,
|
|
54
|
+
type TicketStreamEvent,
|
|
55
|
+
type TicketStreamEventType,
|
|
56
|
+
type TicketMessageStreamData,
|
|
57
|
+
type TicketUnreadSummary,
|
|
48
58
|
TOAST_COPY as TICKET_TOAST_COPY,
|
|
49
59
|
isOptimistic,
|
|
50
60
|
} from './types'
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* callbacks; we don't reach into the QueryClient.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { useEffect, useRef } from 'react'
|
|
24
25
|
import { useStickToBottom } from 'use-stick-to-bottom'
|
|
25
26
|
import { Button } from './../ui/button'
|
|
26
27
|
import { useChatIdentity } from './../chat/hooks/use-chat-identity'
|
|
@@ -40,13 +41,14 @@ import type {
|
|
|
40
41
|
TicketEngagementFile,
|
|
41
42
|
} from './hooks/use-ticket-engagements'
|
|
42
43
|
import { TicketLinkedDeliveryCard } from './ticket-linked-delivery-card'
|
|
44
|
+
import { useOptionalTicketLive } from './ticket-live-provider'
|
|
43
45
|
import { TicketReplyComposer } from './ticket-reply-composer'
|
|
44
46
|
import type {
|
|
45
47
|
AnyTicket,
|
|
46
48
|
TicketAssignedOwner,
|
|
47
49
|
MappedTicketActionError,
|
|
48
50
|
} from './types'
|
|
49
|
-
import { isOptimistic,
|
|
51
|
+
import { isOptimistic, TICKET_MARK_READ_DEBOUNCE_MS } from './types'
|
|
50
52
|
|
|
51
53
|
/** Identity bundle threaded through the action callbacks: local mirror
|
|
52
54
|
* UUID + HubSpot external_id. Actions send `external_id` to HubSpot
|
|
@@ -204,18 +206,48 @@ function TicketTimelinePanel({ ticket }: { ticket: AnyTicket }) {
|
|
|
204
206
|
// Optimistic placeholders don't have a real external_id yet — skip
|
|
205
207
|
// the engagement fetch until the real ticket lands.
|
|
206
208
|
const externalId = isOptimistic(ticket) ? null : ticket.external_id
|
|
207
|
-
// Live conversation refresh
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
209
|
+
// Live conversation refresh is PUSH-driven: `TicketLiveProvider`
|
|
210
|
+
// invalidates `['ticket-engagements', id]` on stream events, so new
|
|
211
|
+
// agent replies surface without any interval (polling was deleted
|
|
212
|
+
// 2026-08; `refetchOnWindowFocus` remains the no-stream fallback).
|
|
213
|
+
const { engagements, isLoading } = useTicketEngagements(externalId, !!externalId)
|
|
214
|
+
|
|
215
|
+
// Read-state lifecycle — this panel mounts exactly while the drawer is
|
|
216
|
+
// open, so it owns: register the open ticket (provider suppresses
|
|
217
|
+
// unread bumps + masks the row), markRead on open, debounced markRead
|
|
218
|
+
// as new messages stream in, flush on close. The provider's callbacks
|
|
219
|
+
// are stable (useCallback) so these effects don't re-fire on unrelated
|
|
220
|
+
// context-value changes.
|
|
221
|
+
const live = useOptionalTicketLive()
|
|
222
|
+
const setOpenTicketId = live?.setOpenTicketId
|
|
223
|
+
const markRead = live?.markRead
|
|
224
|
+
const markReadDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!externalId || !setOpenTicketId || !markRead) return
|
|
228
|
+
setOpenTicketId(externalId)
|
|
229
|
+
void markRead(externalId)
|
|
230
|
+
return () => {
|
|
231
|
+
// Flush a pending debounced markRead on close so a reply that
|
|
232
|
+
// arrived moments before closing is still receipted.
|
|
233
|
+
if (markReadDebounceRef.current) {
|
|
234
|
+
clearTimeout(markReadDebounceRef.current)
|
|
235
|
+
markReadDebounceRef.current = null
|
|
236
|
+
void markRead(externalId)
|
|
237
|
+
}
|
|
238
|
+
setOpenTicketId(null)
|
|
239
|
+
}
|
|
240
|
+
}, [externalId, setOpenTicketId, markRead])
|
|
241
|
+
|
|
242
|
+
const engagementsCount = engagements.length
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (!externalId || !markRead || engagementsCount === 0) return
|
|
245
|
+
if (markReadDebounceRef.current) clearTimeout(markReadDebounceRef.current)
|
|
246
|
+
markReadDebounceRef.current = setTimeout(() => {
|
|
247
|
+
markReadDebounceRef.current = null
|
|
248
|
+
void markRead(externalId)
|
|
249
|
+
}, TICKET_MARK_READ_DEBOUNCE_MS)
|
|
250
|
+
}, [engagementsCount, externalId, markRead])
|
|
219
251
|
|
|
220
252
|
// Slack-style auto-tail (same lib mechanism `ChatMessageList` uses): jump to
|
|
221
253
|
// the newest message on open (`initial:'instant'`), smooth-scroll on a new
|