@flamingo-stack/openframe-frontend-core 0.0.537 → 0.0.538-1791.4733.1
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/chat-protocol/events.d.ts +15 -1
- package/dist/chat-protocol/events.d.ts.map +1 -1
- package/dist/chat-protocol/index.cjs +15 -0
- package/dist/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js +15 -0
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
- package/dist/{chunk-7FXZHI5D.js → chunk-4BK5Z763.js} +2 -2
- package/dist/{chunk-C4LEH3VE.js → chunk-56QT3JBZ.js} +2 -2
- package/dist/{chunk-LFMIAPBP.js → chunk-67CKAX7Z.js} +2 -2
- package/dist/{chunk-CZHCXCWL.js → chunk-73W4L656.js} +2 -2
- package/dist/{chunk-TCSSYTDN.js → chunk-7CBQWZAH.js} +4 -4
- package/dist/{chunk-YIDGB5KP.js → chunk-BNF6FZLP.js} +4 -4
- package/dist/{chunk-CATY4DVR.cjs → chunk-BUKQ6B65.cjs} +87 -87
- package/dist/{chunk-CATY4DVR.cjs.map → chunk-BUKQ6B65.cjs.map} +1 -1
- package/dist/{chunk-KKZ2QMQD.cjs → chunk-C6776X43.cjs} +26 -26
- package/dist/{chunk-KKZ2QMQD.cjs.map → chunk-C6776X43.cjs.map} +1 -1
- package/dist/{chunk-MOIHWYSA.js → chunk-C6NKOHFW.js} +2 -2
- package/dist/{chunk-HRC56BQW.js → chunk-EH6LNTOT.js} +5 -5
- package/dist/{chunk-YPI3JIR6.cjs → chunk-F4QOOHZL.cjs} +11 -11
- package/dist/{chunk-YPI3JIR6.cjs.map → chunk-F4QOOHZL.cjs.map} +1 -1
- package/dist/{chunk-3GXMS4OP.js → chunk-KUKHUECE.js} +2 -2
- package/dist/{chunk-DHSVBGOX.cjs → chunk-LJW7B6GY.cjs} +9 -9
- package/dist/{chunk-DHSVBGOX.cjs.map → chunk-LJW7B6GY.cjs.map} +1 -1
- package/dist/{chunk-WJIBB3KT.cjs → chunk-MGYNZ7SA.cjs} +31 -31
- package/dist/{chunk-WJIBB3KT.cjs.map → chunk-MGYNZ7SA.cjs.map} +1 -1
- package/dist/{chunk-OYTM3YXO.cjs → chunk-NE67LT3B.cjs} +7 -7
- package/dist/{chunk-OYTM3YXO.cjs.map → chunk-NE67LT3B.cjs.map} +1 -1
- package/dist/{chunk-SKIM263K.cjs → chunk-NUHDE7CB.cjs} +37 -37
- package/dist/{chunk-SKIM263K.cjs.map → chunk-NUHDE7CB.cjs.map} +1 -1
- package/dist/{chunk-MDP3YKFI.cjs → chunk-PFGRHRBO.cjs} +5 -5
- package/dist/{chunk-MDP3YKFI.cjs.map → chunk-PFGRHRBO.cjs.map} +1 -1
- package/dist/{chunk-SGNWSUL3.js → chunk-QK4SQAYG.js} +6 -6
- package/dist/{chunk-YMPRMZDD.cjs → chunk-QQ3HUSDJ.cjs} +599 -328
- package/dist/chunk-QQ3HUSDJ.cjs.map +1 -0
- package/dist/{chunk-QPPG3KHW.cjs → chunk-QU6P2U2Q.cjs} +3 -3
- package/dist/{chunk-QPPG3KHW.cjs.map → chunk-QU6P2U2Q.cjs.map} +1 -1
- package/dist/{chunk-V4D5MPB7.cjs → chunk-R3XO2KO4.cjs} +4 -4
- package/dist/{chunk-V4D5MPB7.cjs.map → chunk-R3XO2KO4.cjs.map} +1 -1
- package/dist/{chunk-7EHGP4QK.js → chunk-RBBWKSRC.js} +4638 -4367
- package/dist/chunk-RBBWKSRC.js.map +1 -0
- package/dist/{chunk-VSIHO7DJ.cjs → chunk-TWVRCPWY.cjs} +61 -61
- package/dist/{chunk-VSIHO7DJ.cjs.map → chunk-TWVRCPWY.cjs.map} +1 -1
- package/dist/{chunk-CQQXB25K.js → chunk-UBEVU6LS.js} +2 -2
- package/dist/{chunk-7WNJ7RFO.cjs → chunk-YO5BWF4L.cjs} +14 -14
- package/dist/{chunk-7WNJ7RFO.cjs.map → chunk-YO5BWF4L.cjs.map} +1 -1
- package/dist/{chunk-WHUEUACV.js → chunk-ZBRKTF6S.js} +2 -2
- package/dist/{chunk-5JPSQOZQ.js → chunk-ZHW5VLHY.js} +2 -2
- package/dist/{chunk-YEKXTTAC.cjs → chunk-ZNWW4WAT.cjs} +26 -26
- package/dist/{chunk-YEKXTTAC.cjs.map → chunk-ZNWW4WAT.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/ai-assistant-info.d.ts +31 -0
- package/dist/components/chat/ai-assistant-info.d.ts.map +1 -0
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +8 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +2 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +7 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
- package/dist/components/chat/ticket-escalated-message.d.ts +2 -8
- package/dist/components/chat/ticket-escalated-message.d.ts.map +1 -1
- package/dist/components/chat/ticket-event-message.d.ts +24 -0
- package/dist/components/chat/ticket-event-message.d.ts.map +1 -0
- package/dist/components/chat/types/message.types.d.ts +51 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +13 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +144 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +14 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +13 -1
- package/dist/components/ui/reopen-ticket-modal.d.ts +42 -0
- package/dist/components/ui/reopen-ticket-modal.d.ts.map +1 -0
- package/dist/components/ui/take-over-ticket-modal.d.ts +5 -0
- package/dist/components/ui/take-over-ticket-modal.d.ts.map +1 -1
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +44 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +32 -0
- package/src/chat-protocol/events.ts +16 -0
- package/src/chat-protocol/nats-decoder.ts +27 -0
- package/src/components/chat/__tests__/ticket-event.test.ts +194 -0
- package/src/components/chat/ai-assistant-info.tsx +71 -0
- package/src/components/chat/chat-message-enhanced.tsx +3 -0
- package/src/components/chat/index.ts +2 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +19 -0
- package/src/components/chat/stream/message-mutations.ts +5 -0
- package/src/components/chat/ticket-escalated-message.tsx +10 -32
- package/src/components/chat/ticket-event-message.tsx +79 -0
- package/src/components/chat/types/message.types.ts +60 -1
- package/src/components/chat/utils/message-segment-accumulator.ts +41 -0
- package/src/components/chat/utils/process-historical-messages.ts +31 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/reopen-ticket-modal.tsx +174 -0
- package/src/components/ui/take-over-ticket-modal.tsx +7 -4
- package/src/stories/ReopenTicketModal.stories.tsx +114 -0
- package/src/stories/TicketEventMessage.stories.tsx +100 -0
- package/dist/chunk-7EHGP4QK.js.map +0 -1
- package/dist/chunk-YMPRMZDD.cjs.map +0 -1
- /package/dist/{chunk-7FXZHI5D.js.map → chunk-4BK5Z763.js.map} +0 -0
- /package/dist/{chunk-C4LEH3VE.js.map → chunk-56QT3JBZ.js.map} +0 -0
- /package/dist/{chunk-LFMIAPBP.js.map → chunk-67CKAX7Z.js.map} +0 -0
- /package/dist/{chunk-CZHCXCWL.js.map → chunk-73W4L656.js.map} +0 -0
- /package/dist/{chunk-TCSSYTDN.js.map → chunk-7CBQWZAH.js.map} +0 -0
- /package/dist/{chunk-YIDGB5KP.js.map → chunk-BNF6FZLP.js.map} +0 -0
- /package/dist/{chunk-MOIHWYSA.js.map → chunk-C6NKOHFW.js.map} +0 -0
- /package/dist/{chunk-HRC56BQW.js.map → chunk-EH6LNTOT.js.map} +0 -0
- /package/dist/{chunk-3GXMS4OP.js.map → chunk-KUKHUECE.js.map} +0 -0
- /package/dist/{chunk-SGNWSUL3.js.map → chunk-QK4SQAYG.js.map} +0 -0
- /package/dist/{chunk-CQQXB25K.js.map → chunk-UBEVU6LS.js.map} +0 -0
- /package/dist/{chunk-WHUEUACV.js.map → chunk-ZBRKTF6S.js.map} +0 -0
- /package/dist/{chunk-5JPSQOZQ.js.map → chunk-ZHW5VLHY.js.map} +0 -0
|
@@ -18,6 +18,7 @@ export const MESSAGE_TYPE = {
|
|
|
18
18
|
APPROVAL_RESULT: 'APPROVAL_RESULT',
|
|
19
19
|
ESCALATION_OFFER: 'ESCALATION_OFFER',
|
|
20
20
|
TICKET_ESCALATED: 'TICKET_ESCALATED',
|
|
21
|
+
TICKET_EVENT: 'TICKET_EVENT',
|
|
21
22
|
ERROR: 'ERROR',
|
|
22
23
|
MESSAGE_START: 'MESSAGE_START',
|
|
23
24
|
MESSAGE_END: 'MESSAGE_END',
|
|
@@ -158,6 +159,34 @@ export interface TicketEscalatedData {
|
|
|
158
159
|
text?: string
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
// ========== Ticket Event Types ==========
|
|
163
|
+
|
|
164
|
+
/** Known `TicketEventData.kind` values. The vocabulary is OPEN on the wire:
|
|
165
|
+
* an unknown kind still renders (as a neutral info line) rather than being
|
|
166
|
+
* dropped, so the backend can add lifecycle kinds without a client release. */
|
|
167
|
+
export const TICKET_EVENT_KIND = {
|
|
168
|
+
RESOLVED: 'RESOLVED',
|
|
169
|
+
REOPENED: 'REOPENED',
|
|
170
|
+
} as const
|
|
171
|
+
|
|
172
|
+
export type TicketEventKind =
|
|
173
|
+
| typeof TICKET_EVENT_KIND[keyof typeof TICKET_EVENT_KIND]
|
|
174
|
+
| (string & {})
|
|
175
|
+
|
|
176
|
+
/** Ticket lifecycle receipt (`TICKET_EVENT`) — the ticket was resolved,
|
|
177
|
+
* reopened, etc. Same field names live (NATS chunk) and persisted
|
|
178
|
+
* (GraphQL `TicketEventData` inside `messageData`), so ONE mapper covers
|
|
179
|
+
* both paths. All actor fields are optional: the event stays renderable
|
|
180
|
+
* when the backend can't attribute it. */
|
|
181
|
+
export interface TicketEventData {
|
|
182
|
+
kind: TicketEventKind
|
|
183
|
+
actorId?: string
|
|
184
|
+
actorName?: string
|
|
185
|
+
/** Who acted — e.g. an AI agent vs a human. Open string like `kind`. */
|
|
186
|
+
actorType?: string
|
|
187
|
+
reason?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
161
190
|
/**
|
|
162
191
|
* Single tool call inside a batch approval request.
|
|
163
192
|
* Mirrors backend PendingToolCallDto.
|
|
@@ -310,6 +339,23 @@ export type TicketEscalatedSegment = {
|
|
|
310
339
|
data: TicketEscalatedData
|
|
311
340
|
}
|
|
312
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Ticket lifecycle receipt (resolved / reopened / an unknown future kind).
|
|
344
|
+
* Arrives as a standalone `TICKET_EVENT` chunk outside MESSAGE_START/END —
|
|
345
|
+
* same delivery shape as `ticket_escalated`.
|
|
346
|
+
*
|
|
347
|
+
* `streamSeq` is the chunk's JetStream sequence, kept ON the segment because
|
|
348
|
+
* it is the event's only stable identity: the accumulator upserts on it so a
|
|
349
|
+
* redelivered chunk (catch-up over hydrated history) replaces rather than
|
|
350
|
+
* stacks a second card, while two genuinely distinct events of the same kind
|
|
351
|
+
* (resolve → reopen → resolve) keep distinct sequences and both render.
|
|
352
|
+
*/
|
|
353
|
+
export type TicketEventSegment = {
|
|
354
|
+
type: 'ticket_event'
|
|
355
|
+
data: TicketEventData
|
|
356
|
+
streamSeq?: number
|
|
357
|
+
}
|
|
358
|
+
|
|
313
359
|
export type ErrorSegment = {
|
|
314
360
|
type: 'error'
|
|
315
361
|
title: string
|
|
@@ -322,7 +368,7 @@ export type ContextCompactionSegment = {
|
|
|
322
368
|
summary?: string
|
|
323
369
|
}
|
|
324
370
|
|
|
325
|
-
export type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | AskSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | EscalationOfferSegment | TicketEscalatedSegment | ErrorSegment | ContextCompactionSegment
|
|
371
|
+
export type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | AskSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | EscalationOfferSegment | TicketEscalatedSegment | TicketEventSegment | ErrorSegment | ContextCompactionSegment
|
|
326
372
|
|
|
327
373
|
export type MessageContent = string | MessageSegment[]
|
|
328
374
|
|
|
@@ -427,6 +473,18 @@ export interface TicketEscalatedMessageData extends MessageDataBase {
|
|
|
427
473
|
text?: string
|
|
428
474
|
}
|
|
429
475
|
|
|
476
|
+
/** Persisted ticket lifecycle row — same field names as the live chunk
|
|
477
|
+
* (see `TicketEventData`), decoded by the SAME mapper on replay. Fields are
|
|
478
|
+
* wire-shaped (`| null`): every one is a nullable GraphQL scalar. */
|
|
479
|
+
export interface TicketEventMessageData extends MessageDataBase {
|
|
480
|
+
type: 'TICKET_EVENT'
|
|
481
|
+
kind?: string | null
|
|
482
|
+
actorId?: string | null
|
|
483
|
+
actorName?: string | null
|
|
484
|
+
actorType?: string | null
|
|
485
|
+
reason?: string | null
|
|
486
|
+
}
|
|
487
|
+
|
|
430
488
|
export interface ErrorMessageData extends MessageDataBase {
|
|
431
489
|
type: 'ERROR'
|
|
432
490
|
error?: string
|
|
@@ -473,6 +531,7 @@ export type MessageData =
|
|
|
473
531
|
| ApprovalResultMessageData
|
|
474
532
|
| EscalationOfferMessageData
|
|
475
533
|
| TicketEscalatedMessageData
|
|
534
|
+
| TicketEventMessageData
|
|
476
535
|
| ErrorMessageData
|
|
477
536
|
| AIMetadataMessageData
|
|
478
537
|
| SystemMessageData
|
|
@@ -20,6 +20,8 @@ import type {
|
|
|
20
20
|
EscalationOfferSegment,
|
|
21
21
|
TicketEscalatedData,
|
|
22
22
|
TicketEscalatedSegment,
|
|
23
|
+
TicketEventData,
|
|
24
|
+
TicketEventSegment,
|
|
23
25
|
ContextCompactionSegment,
|
|
24
26
|
ErrorSegment,
|
|
25
27
|
PendingApproval,
|
|
@@ -535,6 +537,42 @@ export class MessageSegmentAccumulator {
|
|
|
535
537
|
return this.getSegments()
|
|
536
538
|
}
|
|
537
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Add a ticket lifecycle receipt (resolved / reopened / unknown kind).
|
|
542
|
+
*
|
|
543
|
+
* Upsert identity is the chunk's stream sequence when BOTH sides know it;
|
|
544
|
+
* otherwise full payload equality. History-hydrated segments come back
|
|
545
|
+
* seq-less (the persisted row's seq lives on the message, not the
|
|
546
|
+
* `messageData`), so a JetStream catch-up redelivery of the same event must
|
|
547
|
+
* still match its hydrated twin — payload equality is what catches that.
|
|
548
|
+
* Two genuinely distinct events (resolve → reopen → resolve) differ in seq
|
|
549
|
+
* or payload and both render.
|
|
550
|
+
*/
|
|
551
|
+
addTicketEvent(data: TicketEventData, streamSeq?: number): MessageSegment[] {
|
|
552
|
+
const segment: TicketEventSegment = {
|
|
553
|
+
type: 'ticket_event',
|
|
554
|
+
data,
|
|
555
|
+
...(streamSeq !== undefined ? { streamSeq } : {}),
|
|
556
|
+
}
|
|
557
|
+
const existingIndex = this.segments.findIndex((s) => {
|
|
558
|
+
if (s.type !== 'ticket_event') return false
|
|
559
|
+
if (s.streamSeq !== undefined && streamSeq !== undefined) return s.streamSeq === streamSeq
|
|
560
|
+
return (
|
|
561
|
+
s.data.kind === data.kind &&
|
|
562
|
+
s.data.actorId === data.actorId &&
|
|
563
|
+
s.data.actorName === data.actorName &&
|
|
564
|
+
s.data.actorType === data.actorType &&
|
|
565
|
+
s.data.reason === data.reason
|
|
566
|
+
)
|
|
567
|
+
})
|
|
568
|
+
if (existingIndex !== -1) {
|
|
569
|
+
this.segments[existingIndex] = segment
|
|
570
|
+
return this.getSegments()
|
|
571
|
+
}
|
|
572
|
+
this.segments.push(segment)
|
|
573
|
+
return this.getSegments()
|
|
574
|
+
}
|
|
575
|
+
|
|
538
576
|
/**
|
|
539
577
|
* Update status of an existing approval segment (single, batch, or
|
|
540
578
|
* escalation offer).
|
|
@@ -680,6 +718,9 @@ export class MessageSegmentAccumulator {
|
|
|
680
718
|
case 'ticket_escalated':
|
|
681
719
|
this.addTicketEscalated(segment.data)
|
|
682
720
|
break
|
|
721
|
+
case 'ticket_event':
|
|
722
|
+
this.addTicketEvent(segment.data, segment.streamSeq)
|
|
723
|
+
break
|
|
683
724
|
case 'error':
|
|
684
725
|
this.addError(segment.title, segment.details)
|
|
685
726
|
break
|
|
@@ -243,6 +243,23 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
|
|
|
243
243
|
}
|
|
244
244
|
return null
|
|
245
245
|
|
|
246
|
+
// Same completeness gate as the live decoder: `kind` (an OPEN string —
|
|
247
|
+
// unknown kinds render as a neutral line) is the only required field.
|
|
248
|
+
// Field names match the chunk, so ONE mapping covers both paths; the
|
|
249
|
+
// typeof gates also fold the row's GraphQL nulls to undefined.
|
|
250
|
+
case MESSAGE_TYPE.TICKET_EVENT: {
|
|
251
|
+
const kind = 'kind' in data && typeof data.kind === 'string' ? data.kind.trim() : ''
|
|
252
|
+
if (!kind) return null
|
|
253
|
+
return {
|
|
254
|
+
type: 'ticket-event',
|
|
255
|
+
kind,
|
|
256
|
+
actorId: typeof data.actorId === 'string' ? data.actorId : undefined,
|
|
257
|
+
actorName: typeof data.actorName === 'string' ? data.actorName : undefined,
|
|
258
|
+
actorType: typeof data.actorType === 'string' ? data.actorType : undefined,
|
|
259
|
+
reason: typeof data.reason === 'string' && data.reason.trim() ? data.reason : undefined,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
246
263
|
case MESSAGE_TYPE.ERROR:
|
|
247
264
|
if ('error' in data) {
|
|
248
265
|
return {
|
|
@@ -336,6 +353,20 @@ function applyHistoryEvent(
|
|
|
336
353
|
})
|
|
337
354
|
break
|
|
338
355
|
|
|
356
|
+
// Seq-less on purpose: the persisted row's sequence lives on the message
|
|
357
|
+
// (`lastChunkStreamSeq`), not in `messageData`. A live redelivery of the
|
|
358
|
+
// same event matches this segment by payload equality (see
|
|
359
|
+
// `addTicketEvent`).
|
|
360
|
+
case 'ticket-event':
|
|
361
|
+
accumulator.addTicketEvent({
|
|
362
|
+
kind: event.kind,
|
|
363
|
+
actorId: event.actorId,
|
|
364
|
+
actorName: event.actorName,
|
|
365
|
+
actorType: event.actorType,
|
|
366
|
+
reason: event.reason,
|
|
367
|
+
})
|
|
368
|
+
break
|
|
369
|
+
|
|
339
370
|
case 'escalation-offer-resolved':
|
|
340
371
|
// Recorded, not applied here: `applyOfferResolutions` runs after every
|
|
341
372
|
// flush and covers the same-bubble case as well as the cross-bubble one.
|
|
@@ -179,6 +179,7 @@ export * from './test-run'
|
|
|
179
179
|
export * from './assignee-dropdown'
|
|
180
180
|
export * from './ticket-info-section'
|
|
181
181
|
export * from './take-over-ticket-modal'
|
|
182
|
+
export * from './reopen-ticket-modal'
|
|
182
183
|
export * from './ticket-detail-section'
|
|
183
184
|
export * from './ticket-attachments-list'
|
|
184
185
|
export * from './ticket-note-card'
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Autocomplete } from './autocomplete';
|
|
5
|
+
import { Button } from './button';
|
|
6
|
+
import { ColorSwatch } from './color-swatch';
|
|
7
|
+
import { ModalV2, ModalV2Footer, ModalV2Header, ModalV2Title } from './modal-v2';
|
|
8
|
+
import { SquareAvatar } from './square-avatar';
|
|
9
|
+
import {
|
|
10
|
+
renderTicketAssigneeOption,
|
|
11
|
+
renderTicketStatusOption,
|
|
12
|
+
type TakeOverAssigneeOption,
|
|
13
|
+
type TakeOverStatusOption,
|
|
14
|
+
} from './take-over-ticket-modal';
|
|
15
|
+
import { Textarea } from './textarea';
|
|
16
|
+
|
|
17
|
+
/** Backend trims and caps the reason at 1000 chars; cap it client-side too. */
|
|
18
|
+
const REASON_MAX_LENGTH = 1000;
|
|
19
|
+
|
|
20
|
+
export interface ReopenTicketSelection {
|
|
21
|
+
statusId: string;
|
|
22
|
+
/** `null` — reopen without an assignee (the ticket stays unassigned). */
|
|
23
|
+
assigneeId: string | null;
|
|
24
|
+
/** Trimmed; `null` when the field was left blank. */
|
|
25
|
+
reason: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ReopenTicketModalProps {
|
|
29
|
+
isOpen: boolean;
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
/** Highlighted ticket reference in the description, e.g. "1003: Email client synchronization issues". */
|
|
32
|
+
ticketRef: string;
|
|
33
|
+
/** Allowed target statuses, in display order. Per design, the consumer
|
|
34
|
+
* pre-selects Tech Required by default (annotation on the mock: "tech
|
|
35
|
+
* required by default, but may be changed to custom status"). */
|
|
36
|
+
statusOptions: TakeOverStatusOption[];
|
|
37
|
+
/** Assignable users, in display order (consumer decides ordering, e.g. signed-in user first). */
|
|
38
|
+
assigneeOptions: TakeOverAssigneeOption[];
|
|
39
|
+
assigneesLoading?: boolean;
|
|
40
|
+
/** Pre-selected status; falls back to the first status option. */
|
|
41
|
+
initialStatusId?: string | null;
|
|
42
|
+
/** Pre-selected assignee. Unlike Take Over, the assignee is OPTIONAL —
|
|
43
|
+
* a reopened ticket may stay unassigned (it is flagged for triage). */
|
|
44
|
+
initialAssigneeId?: string | null;
|
|
45
|
+
/** Confirm in-flight: buttons lock and the modal refuses to close. */
|
|
46
|
+
isPending?: boolean;
|
|
47
|
+
onConfirm: (selection: ReopenTicketSelection) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reopen Ticket dialog (Figma openframe---tickets 8456-17581): shown when a
|
|
52
|
+
* technician reopens a Resolved/Archived ticket. Same Status + Assigned pair
|
|
53
|
+
* as {@link TakeOverTicketModal} plus an optional free-text reason.
|
|
54
|
+
* Presentational — the consumer supplies the option lists, their ordering and
|
|
55
|
+
* default selections, and performs the actual reopen in `onConfirm`.
|
|
56
|
+
*
|
|
57
|
+
* Desktop footer shows only the accent Reopen button (X closes); mobile
|
|
58
|
+
* bottom-sheet adds a Cancel button, matching Take Over.
|
|
59
|
+
*/
|
|
60
|
+
export function ReopenTicketModal({
|
|
61
|
+
isOpen,
|
|
62
|
+
onClose,
|
|
63
|
+
ticketRef,
|
|
64
|
+
statusOptions,
|
|
65
|
+
assigneeOptions,
|
|
66
|
+
assigneesLoading,
|
|
67
|
+
initialStatusId,
|
|
68
|
+
initialAssigneeId,
|
|
69
|
+
isPending = false,
|
|
70
|
+
onConfirm,
|
|
71
|
+
}: ReopenTicketModalProps) {
|
|
72
|
+
const [statusId, setStatusId] = React.useState<string | null>(null);
|
|
73
|
+
const [assigneeId, setAssigneeId] = React.useState<string | null>(null);
|
|
74
|
+
const [reason, setReason] = React.useState('');
|
|
75
|
+
|
|
76
|
+
// Re-seed the selections every time the modal opens.
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
if (!isOpen) return;
|
|
79
|
+
setStatusId(initialStatusId ?? null);
|
|
80
|
+
setAssigneeId(initialAssigneeId ?? null);
|
|
81
|
+
setReason('');
|
|
82
|
+
}, [isOpen, initialStatusId, initialAssigneeId]);
|
|
83
|
+
|
|
84
|
+
const selectedStatusId = statusId ?? statusOptions[0]?.value ?? null;
|
|
85
|
+
const selectedStatus = statusOptions.find(o => o.value === selectedStatusId);
|
|
86
|
+
const selectedAssignee = assigneeOptions.find(o => o.value === assigneeId);
|
|
87
|
+
|
|
88
|
+
const handleConfirm = () => {
|
|
89
|
+
if (!selectedStatusId || isPending) return;
|
|
90
|
+
const trimmedReason = reason.trim();
|
|
91
|
+
onConfirm({
|
|
92
|
+
statusId: selectedStatusId,
|
|
93
|
+
assigneeId: assigneeId ?? null,
|
|
94
|
+
reason: trimmedReason ? trimmedReason.slice(0, REASON_MAX_LENGTH) : null,
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className="text-left md:max-w-[600px]">
|
|
100
|
+
<ModalV2Header>
|
|
101
|
+
<ModalV2Title>Reopen Ticket</ModalV2Title>
|
|
102
|
+
</ModalV2Header>
|
|
103
|
+
|
|
104
|
+
<p className="text-h4 text-ods-text-primary">
|
|
105
|
+
The ticket <span className="text-ods-accent">{ticketRef}</span> will return to the selected status and appear on
|
|
106
|
+
the board. Assigned admins and the user will be notified.
|
|
107
|
+
</p>
|
|
108
|
+
|
|
109
|
+
<div className="flex flex-col md:flex-row gap-[var(--spacing-system-l)] w-full">
|
|
110
|
+
<div className="flex-1 min-w-0">
|
|
111
|
+
<Autocomplete
|
|
112
|
+
label="Status"
|
|
113
|
+
options={statusOptions}
|
|
114
|
+
value={selectedStatusId}
|
|
115
|
+
onChange={setStatusId}
|
|
116
|
+
placeholder="Select Status"
|
|
117
|
+
startAdornment={selectedStatus ? <ColorSwatch color={selectedStatus.color} /> : undefined}
|
|
118
|
+
renderOption={renderTicketStatusOption}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="flex-1 min-w-0">
|
|
122
|
+
<Autocomplete
|
|
123
|
+
label="Assigned"
|
|
124
|
+
options={assigneeOptions}
|
|
125
|
+
value={assigneeId}
|
|
126
|
+
onChange={setAssigneeId}
|
|
127
|
+
placeholder="Select Technician"
|
|
128
|
+
loading={assigneesLoading}
|
|
129
|
+
startAdornment={
|
|
130
|
+
selectedAssignee ? (
|
|
131
|
+
<SquareAvatar
|
|
132
|
+
src={selectedAssignee.imageUrl}
|
|
133
|
+
alt={selectedAssignee.label}
|
|
134
|
+
fallback={selectedAssignee.label}
|
|
135
|
+
size="sm"
|
|
136
|
+
variant="round"
|
|
137
|
+
/>
|
|
138
|
+
) : undefined
|
|
139
|
+
}
|
|
140
|
+
renderOption={renderTicketAssigneeOption}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="w-full">
|
|
146
|
+
<Textarea
|
|
147
|
+
label="Reason"
|
|
148
|
+
value={reason}
|
|
149
|
+
onChange={event => setReason(event.target.value)}
|
|
150
|
+
placeholder="Describe what's still not working (optional)"
|
|
151
|
+
maxLength={REASON_MAX_LENGTH}
|
|
152
|
+
disabled={isPending}
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<ModalV2Footer>
|
|
157
|
+
<Button type="button" variant="outline" onClick={onClose} disabled={isPending} className="flex-1 md:hidden">
|
|
158
|
+
Cancel
|
|
159
|
+
</Button>
|
|
160
|
+
<div className="hidden md:block flex-1" />
|
|
161
|
+
<Button
|
|
162
|
+
type="button"
|
|
163
|
+
variant="accent"
|
|
164
|
+
onClick={handleConfirm}
|
|
165
|
+
loading={isPending}
|
|
166
|
+
disabled={!selectedStatusId}
|
|
167
|
+
className="flex-1"
|
|
168
|
+
>
|
|
169
|
+
Reopen Ticket
|
|
170
|
+
</Button>
|
|
171
|
+
</ModalV2Footer>
|
|
172
|
+
</ModalV2>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -42,7 +42,9 @@ export interface TakeOverTicketModalProps {
|
|
|
42
42
|
onConfirm: (selection: TakeOverTicketSelection) => void;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
/** Status row with color swatch — shared by the ticket lifecycle modals
|
|
46
|
+
* (Take Over / Reopen), which present the same Status + Assigned pair. */
|
|
47
|
+
export function renderTicketStatusOption(option: AutocompleteOption, isSelected: boolean): React.ReactNode {
|
|
46
48
|
const { label, color } = option as TakeOverStatusOption;
|
|
47
49
|
return (
|
|
48
50
|
<div className="flex items-center justify-between w-full min-w-0 gap-[var(--spacing-system-xs)]">
|
|
@@ -57,7 +59,8 @@ function renderStatusOption(option: AutocompleteOption, isSelected: boolean): Re
|
|
|
57
59
|
);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
/** Assignee row with round avatar — shared by the ticket lifecycle modals. */
|
|
63
|
+
export function renderTicketAssigneeOption(option: AutocompleteOption, isSelected: boolean): React.ReactNode {
|
|
61
64
|
const { label, imageUrl } = option as TakeOverAssigneeOption;
|
|
62
65
|
return (
|
|
63
66
|
<div className="flex items-center justify-between w-full min-w-0 gap-[var(--spacing-system-xs)]">
|
|
@@ -135,7 +138,7 @@ export function TakeOverTicketModal({
|
|
|
135
138
|
onChange={setStatusId}
|
|
136
139
|
placeholder="Select Status"
|
|
137
140
|
startAdornment={selectedStatus ? <ColorSwatch color={selectedStatus.color} /> : undefined}
|
|
138
|
-
renderOption={
|
|
141
|
+
renderOption={renderTicketStatusOption}
|
|
139
142
|
/>
|
|
140
143
|
</div>
|
|
141
144
|
<div className="flex-1 min-w-0">
|
|
@@ -157,7 +160,7 @@ export function TakeOverTicketModal({
|
|
|
157
160
|
/>
|
|
158
161
|
) : undefined
|
|
159
162
|
}
|
|
160
|
-
renderOption={
|
|
163
|
+
renderOption={renderTicketAssigneeOption}
|
|
161
164
|
/>
|
|
162
165
|
</div>
|
|
163
166
|
</div>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
import { Button } from '../components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
ReopenTicketModal,
|
|
7
|
+
type ReopenTicketModalProps,
|
|
8
|
+
type ReopenTicketSelection,
|
|
9
|
+
} from '../components/ui/reopen-ticket-modal';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'UI/ReopenTicketModal',
|
|
13
|
+
component: ReopenTicketModal,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'centered',
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
'Reopen Ticket dialog (Figma openframe---tickets 8456-17581): shown when a technician reopens a ' +
|
|
20
|
+
'Resolved/Archived ticket. Same Status + Assigned pair as TakeOverTicketModal plus an optional free-text ' +
|
|
21
|
+
'reason. Unlike Take Over, the assignee is optional - a reopened ticket may stay unassigned. ' +
|
|
22
|
+
'Presentational - the consumer supplies option lists (design default pre-selects Tech Required), ' +
|
|
23
|
+
'default selections, and performs the actual reopen in onConfirm.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
isPending: { control: 'boolean' },
|
|
29
|
+
assigneesLoading: { control: 'boolean' },
|
|
30
|
+
},
|
|
31
|
+
} satisfies Meta<typeof ReopenTicketModal>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
const STATUS_OPTIONS = [
|
|
37
|
+
{ value: 'todo', label: 'Todo', color: '#2890fa' },
|
|
38
|
+
{ value: 'in-progress', label: 'In Progress', color: '#ffc008' },
|
|
39
|
+
{ value: 'tech-required', label: 'Technician Required', color: '#f36666' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const ASSIGNEE_OPTIONS = [
|
|
43
|
+
{ value: 'u1', label: 'Roman Smith' },
|
|
44
|
+
{ value: 'u2', label: 'Olivia Carter' },
|
|
45
|
+
{ value: 'u3', label: 'James Wilson' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function ControlledModal(props: Partial<ReopenTicketModalProps> & { pendingOnConfirm?: boolean }) {
|
|
49
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
50
|
+
const [isPending, setIsPending] = useState(false);
|
|
51
|
+
|
|
52
|
+
const handleConfirm = (selection: ReopenTicketSelection) => {
|
|
53
|
+
props.onConfirm?.(selection);
|
|
54
|
+
if (!props.pendingOnConfirm) {
|
|
55
|
+
setIsOpen(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Simulate the consumer's reopen mutation round-trip.
|
|
59
|
+
setIsPending(true);
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
setIsPending(false);
|
|
62
|
+
setIsOpen(false);
|
|
63
|
+
}, 1500);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<Button variant="outline" onClick={() => setIsOpen(true)}>
|
|
69
|
+
Reopen Ticket
|
|
70
|
+
</Button>
|
|
71
|
+
<ReopenTicketModal
|
|
72
|
+
isOpen={isOpen}
|
|
73
|
+
onClose={() => setIsOpen(false)}
|
|
74
|
+
ticketRef="1003: Email client synchronization issues"
|
|
75
|
+
statusOptions={STATUS_OPTIONS}
|
|
76
|
+
assigneeOptions={ASSIGNEE_OPTIONS}
|
|
77
|
+
{...props}
|
|
78
|
+
isPending={props.isPending ?? isPending}
|
|
79
|
+
onConfirm={handleConfirm}
|
|
80
|
+
/>
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Design default: Tech Required pre-selected (mock annotation), previous assignee restored. */
|
|
86
|
+
export const Default: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
isOpen: true,
|
|
89
|
+
onClose: fn(),
|
|
90
|
+
onConfirm: fn(),
|
|
91
|
+
ticketRef: '1003: Email client synchronization issues',
|
|
92
|
+
statusOptions: STATUS_OPTIONS,
|
|
93
|
+
assigneeOptions: ASSIGNEE_OPTIONS,
|
|
94
|
+
initialStatusId: 'tech-required',
|
|
95
|
+
initialAssigneeId: 'u1',
|
|
96
|
+
},
|
|
97
|
+
render: args => <ControlledModal {...args} />,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* No assignee - the reopen still confirms and the ticket stays unassigned
|
|
102
|
+
* (flagged for triage). This is the deliberate difference from Take Over,
|
|
103
|
+
* where the CTA locks until a technician is picked.
|
|
104
|
+
*/
|
|
105
|
+
export const NoAssignee: Story = {
|
|
106
|
+
args: { ...Default.args, initialAssigneeId: null },
|
|
107
|
+
render: args => <ControlledModal {...args} />,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Confirm shows the pending state (buttons lock, modal refuses to close). */
|
|
111
|
+
export const PendingOnConfirm: Story = {
|
|
112
|
+
args: { ...Default.args },
|
|
113
|
+
render: args => <ControlledModal {...args} pendingOnConfirm />,
|
|
114
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
import { TicketEventMessage } from "../components/chat/ticket-event-message";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ticket_event` message segment — the ticket lifecycle receipt (Figma:
|
|
8
|
+
* `ai-assistant-info`, types resolved-fae / resolved-tech / reopened-fae /
|
|
9
|
+
* reopened-tech / reopened-reason).
|
|
10
|
+
*
|
|
11
|
+
* Decoded from the standalone `TICKET_EVENT` chunk (live) or the persisted
|
|
12
|
+
* `TicketEventData` row (history). `kind` is an OPEN vocabulary: unknown
|
|
13
|
+
* kinds render as a neutral info line rather than being dropped, so the
|
|
14
|
+
* backend can add lifecycle kinds without a client release.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Fixed so the timestamp doesn't churn between snapshots. */
|
|
18
|
+
const TIMESTAMP = new Date("2026-08-07T14:47:00");
|
|
19
|
+
|
|
20
|
+
const plainDecorator = (Story: React.ComponentType) => (
|
|
21
|
+
<div style={{ maxWidth: 600, background: "var(--color-bg)" }} className="p-4">
|
|
22
|
+
<Story />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const meta = {
|
|
27
|
+
title: "Chat/Client/TicketEventMessage",
|
|
28
|
+
component: TicketEventMessage,
|
|
29
|
+
tags: ["autodocs"],
|
|
30
|
+
parameters: {
|
|
31
|
+
docs: {
|
|
32
|
+
description: {
|
|
33
|
+
component:
|
|
34
|
+
"Renders a `TICKET_EVENT` block: a green check for RESOLVED, a pink info tile for REOPENED and for any kind this build doesn't know, with copy composed from the event's actor and reason fields.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
decorators: [plainDecorator],
|
|
39
|
+
} satisfies Meta<typeof TicketEventMessage>;
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof meta>;
|
|
43
|
+
|
|
44
|
+
/** Fae (the AI agent) resolved the ticket — Figma type=resolved-fae. */
|
|
45
|
+
export const ResolvedByFae: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
data: { kind: "RESOLVED", actorId: "fae", actorName: "Fae", actorType: "AI" },
|
|
48
|
+
timestamp: TIMESTAMP,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** A human technician resolved the ticket — Figma type=resolved-tech. */
|
|
53
|
+
export const ResolvedByTechnician: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
data: { kind: "RESOLVED", actorId: "u-42", actorName: "Roman Smith", actorType: "TECHNICIAN" },
|
|
56
|
+
timestamp: TIMESTAMP,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Reopened with the user's stated reason — Figma type=reopened-reason. */
|
|
61
|
+
export const ReopenedWithReason: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
data: {
|
|
64
|
+
kind: "REOPENED",
|
|
65
|
+
actorId: "u-7",
|
|
66
|
+
actorName: "John Smith",
|
|
67
|
+
actorType: "CLIENT",
|
|
68
|
+
reason: "The printer stopped working again",
|
|
69
|
+
},
|
|
70
|
+
timestamp: TIMESTAMP,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Reopened, no reason, AI keeps assisting — Figma type=reopened-fae. */
|
|
75
|
+
export const ReopenedFaeContinues: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
data: { kind: "REOPENED", actorId: "fae", actorName: "Fae", actorType: "AI" },
|
|
78
|
+
timestamp: TIMESTAMP,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Reopened, no reason, routed to a human — Figma type=reopened-tech. */
|
|
83
|
+
export const ReopenedTechnicianReplies: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
data: { kind: "REOPENED", actorId: "u-42", actorName: "Roman Smith", actorType: "TECHNICIAN" },
|
|
86
|
+
timestamp: TIMESTAMP,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A kind the client has never seen. The open vocabulary means the backend can
|
|
92
|
+
* add lifecycle kinds without a client release — this story is the regression
|
|
93
|
+
* guard: neutral info line, humanized title, never dropped.
|
|
94
|
+
*/
|
|
95
|
+
export const UnknownKind: Story = {
|
|
96
|
+
args: {
|
|
97
|
+
data: { kind: "ON_HOLD", actorName: "Roman Smith", reason: "Waiting for replacement hardware" },
|
|
98
|
+
timestamp: TIMESTAMP,
|
|
99
|
+
},
|
|
100
|
+
};
|