Orchestrates all ticket write operations (create, update, close, reopen, reply) through a single POST endpoint, with optimistic UI updates, per-row mutex guards, a serialized mutation queue, and mirror-sync retry logic. ## Key Components ### Hook **`useTicketActions(options)`** — Returns all ticket mutation helpers and associated state selectors. ### Interfaces | Interface | Purpose | |---|---| | `UseTicketActionsOptions` | Inbound callbacks: `prependOptimistic`, `removeOptimistic`, `removeTicketFromCache`, `toast`, `onSupportSystemDown` | | `UseTicketActionsReturn` | Outbound actions and state selectors | | `TicketRef` | Decouples the local mirror UUID (`id`) from the HubSpot external ID (`external_id`) | | `TicketActionResponse` | Flat wire shape from `/api/chat/agent/ticket-action` | ### Internal Mechanisms | Mechanism | Description | |---|---| | `formInFlightRef` + `isSubmittingForm` | Ref/state split prevents duplicate form submits in the same tick | | `busyRowsRef` + `busyRows` | Per-row `Set` mutex; `isRowBusy(localId)` drives row-level UI disable | | `enqueue()` | Depth-1 promise queue serializes all mutations; prevents server rate-limit stampedes | | `watcherControllersRef` | Tracks `AbortController` per placeholder for mirror-sync backoff refetches; cleaned up on unmount | | `replyErrorByTicket` | Persisted `Map` powers the inline reply-failure banner in `` | | `REPLY_BANNER_CODES` | Subset of `TicketActionErrorCode` values that surface in the inline banner vs. toast-only | | `MIRROR_SYNC_BACKOFF_MS` | `[3000, 6000, 12000]` ms retry schedule (~21s cumulative); drops placeholder after exhaustion | ## Usage Example ```typescript const { submitTicket, sendMessage, closeTicket, reopenTicket, isSubmittingForm, isRowBusy, replyErrorFor, clearReplyError, } = useTicketActions({ prependOptimistic: (placeholder) => { queryClient.setQueryData(['tickets'], (prev: TicketData) => ({ ...prev, tickets: [placeholder, ...prev.tickets], })) }, removeOptimistic: (placeholderId) => { /* remove from cache */ }, removeTicketFromCache: (ticketId) => { /* evict row */ }, toast, onSupportSystemDown: () => setSupportSystemDown(true), }) // Create a new ticket const ok = await submitTicket({ subject: 'Outlook keeps crashing', content: 'Happens on every launch since the last update.', attachments: [], }) // Reply with text + attachments in one HubSpot Note await sendMessage( { id: 'uuid-123', external_id: 'hs-456' }, 'Reproduced — pushing a fix now.', [attachment], ) // Close with resolution note await closeTicket( { id: 'uuid-123', external_id: 'hs-456' }, 'Resolved by rolling back the update.', ) // Disable a row while its action is in flight // Show inline reply error banner const replyError = replyErrorFor(ticket.external_id) {replyError && ( clearReplyError(ticket.external_id)} /> )} ``` ## Notes - All mutations route through `executeTicketAction()`, which calls `embedAuthedFetch` with bearer/act-as headers — the same auth path as the chat agent. - `TicketRef.id` (UUID) and `TicketRef.external_id` (HubSpot numeric ID) must be kept distinct; swapping them causes either a React mutex miss or a HubSpot 404. - Source: [`use-ticket-actions.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/use-ticket-actions.ts)