@qoretechnologies/reqraft 0.10.13-pr.74.gccefa54 → 0.10.13

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.
Files changed (125) hide show
  1. package/.claude/CLAUDE.md +8 -11
  2. package/dist/components/form/engine/FormEngine.d.ts +1 -1
  3. package/dist/helpers/common.d.ts +0 -5
  4. package/dist/helpers/common.d.ts.map +1 -1
  5. package/dist/helpers/common.js +1 -20
  6. package/dist/helpers/common.js.map +1 -1
  7. package/dist/index.d.ts +0 -5
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -7
  10. package/dist/index.js.map +1 -1
  11. package/dist/providers/StorageProvider.d.ts +13 -0
  12. package/dist/providers/StorageProvider.d.ts.map +1 -1
  13. package/dist/providers/StorageProvider.js +29 -5
  14. package/dist/providers/StorageProvider.js.map +1 -1
  15. package/package.json +3 -3
  16. package/src/helpers/common.ts +0 -19
  17. package/src/hooks/useStorage/useStorage.stories.tsx +52 -0
  18. package/src/index.tsx +0 -5
  19. package/src/providers/StorageProvider.tsx +30 -4
  20. package/dist/components/composer/Composer.d.ts +0 -108
  21. package/dist/components/composer/Composer.d.ts.map +0 -1
  22. package/dist/components/composer/Composer.js +0 -238
  23. package/dist/components/composer/Composer.js.map +0 -1
  24. package/dist/components/composer/helpers.d.ts +0 -21
  25. package/dist/components/composer/helpers.d.ts.map +0 -1
  26. package/dist/components/composer/helpers.js +0 -75
  27. package/dist/components/composer/helpers.js.map +0 -1
  28. package/dist/components/composer/index.d.ts +0 -3
  29. package/dist/components/composer/index.d.ts.map +0 -1
  30. package/dist/components/composer/index.js +0 -19
  31. package/dist/components/composer/index.js.map +0 -1
  32. package/dist/components/imageLightbox/ImageLightbox.d.ts +0 -17
  33. package/dist/components/imageLightbox/ImageLightbox.d.ts.map +0 -1
  34. package/dist/components/imageLightbox/ImageLightbox.js +0 -129
  35. package/dist/components/imageLightbox/ImageLightbox.js.map +0 -1
  36. package/dist/components/imageLightbox/index.d.ts +0 -2
  37. package/dist/components/imageLightbox/index.d.ts.map +0 -1
  38. package/dist/components/imageLightbox/index.js +0 -18
  39. package/dist/components/imageLightbox/index.js.map +0 -1
  40. package/dist/components/supportTicket/AttachmentChips.d.ts +0 -10
  41. package/dist/components/supportTicket/AttachmentChips.d.ts.map +0 -1
  42. package/dist/components/supportTicket/AttachmentChips.js +0 -119
  43. package/dist/components/supportTicket/AttachmentChips.js.map +0 -1
  44. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +0 -35
  45. package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +0 -1
  46. package/dist/components/supportTicket/InterfaceReferenceTags.js +0 -31
  47. package/dist/components/supportTicket/InterfaceReferenceTags.js.map +0 -1
  48. package/dist/components/supportTicket/ReferencePicker.d.ts +0 -41
  49. package/dist/components/supportTicket/ReferencePicker.d.ts.map +0 -1
  50. package/dist/components/supportTicket/ReferencePicker.js +0 -86
  51. package/dist/components/supportTicket/ReferencePicker.js.map +0 -1
  52. package/dist/components/supportTicket/TicketHeader.d.ts +0 -86
  53. package/dist/components/supportTicket/TicketHeader.d.ts.map +0 -1
  54. package/dist/components/supportTicket/TicketHeader.js +0 -149
  55. package/dist/components/supportTicket/TicketHeader.js.map +0 -1
  56. package/dist/components/supportTicket/TicketMetaTags.d.ts +0 -32
  57. package/dist/components/supportTicket/TicketMetaTags.d.ts.map +0 -1
  58. package/dist/components/supportTicket/TicketMetaTags.js +0 -21
  59. package/dist/components/supportTicket/TicketMetaTags.js.map +0 -1
  60. package/dist/components/supportTicket/TicketReplyBox.d.ts +0 -63
  61. package/dist/components/supportTicket/TicketReplyBox.d.ts.map +0 -1
  62. package/dist/components/supportTicket/TicketReplyBox.js +0 -115
  63. package/dist/components/supportTicket/TicketReplyBox.js.map +0 -1
  64. package/dist/components/supportTicket/fileToAttachment.d.ts +0 -9
  65. package/dist/components/supportTicket/fileToAttachment.d.ts.map +0 -1
  66. package/dist/components/supportTicket/fileToAttachment.js +0 -28
  67. package/dist/components/supportTicket/fileToAttachment.js.map +0 -1
  68. package/dist/components/supportTicket/index.d.ts +0 -9
  69. package/dist/components/supportTicket/index.d.ts.map +0 -1
  70. package/dist/components/supportTicket/index.js +0 -25
  71. package/dist/components/supportTicket/index.js.map +0 -1
  72. package/dist/components/supportTicket/meta.d.ts +0 -63
  73. package/dist/components/supportTicket/meta.d.ts.map +0 -1
  74. package/dist/components/supportTicket/meta.js +0 -79
  75. package/dist/components/supportTicket/meta.js.map +0 -1
  76. package/dist/components/ticketReferences/TicketReferences.d.ts +0 -52
  77. package/dist/components/ticketReferences/TicketReferences.d.ts.map +0 -1
  78. package/dist/components/ticketReferences/TicketReferences.js +0 -453
  79. package/dist/components/ticketReferences/TicketReferences.js.map +0 -1
  80. package/dist/components/ticketReferences/index.d.ts +0 -3
  81. package/dist/components/ticketReferences/index.d.ts.map +0 -1
  82. package/dist/components/ticketReferences/index.js +0 -6
  83. package/dist/components/ticketReferences/index.js.map +0 -1
  84. package/dist/components/ticketThread/TicketThread.d.ts +0 -52
  85. package/dist/components/ticketThread/TicketThread.d.ts.map +0 -1
  86. package/dist/components/ticketThread/TicketThread.js +0 -248
  87. package/dist/components/ticketThread/TicketThread.js.map +0 -1
  88. package/dist/components/ticketThread/index.d.ts +0 -3
  89. package/dist/components/ticketThread/index.d.ts.map +0 -1
  90. package/dist/components/ticketThread/index.js +0 -7
  91. package/dist/components/ticketThread/index.js.map +0 -1
  92. package/dist/helpers/scrollbar.d.ts +0 -2
  93. package/dist/helpers/scrollbar.d.ts.map +0 -1
  94. package/dist/helpers/scrollbar.js +0 -25
  95. package/dist/helpers/scrollbar.js.map +0 -1
  96. package/src/components/composer/Composer.stories.tsx +0 -238
  97. package/src/components/composer/Composer.tsx +0 -545
  98. package/src/components/composer/helpers.ts +0 -74
  99. package/src/components/composer/index.ts +0 -2
  100. package/src/components/imageLightbox/ImageLightbox.stories.tsx +0 -156
  101. package/src/components/imageLightbox/ImageLightbox.tsx +0 -169
  102. package/src/components/imageLightbox/index.ts +0 -1
  103. package/src/components/supportTicket/AttachmentChips.stories.tsx +0 -122
  104. package/src/components/supportTicket/AttachmentChips.tsx +0 -142
  105. package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +0 -134
  106. package/src/components/supportTicket/InterfaceReferenceTags.tsx +0 -85
  107. package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -296
  108. package/src/components/supportTicket/ReferencePicker.tsx +0 -237
  109. package/src/components/supportTicket/TicketConversation.stories.tsx +0 -122
  110. package/src/components/supportTicket/TicketHeader.stories.tsx +0 -183
  111. package/src/components/supportTicket/TicketHeader.tsx +0 -247
  112. package/src/components/supportTicket/TicketMetaTags.stories.tsx +0 -103
  113. package/src/components/supportTicket/TicketMetaTags.tsx +0 -85
  114. package/src/components/supportTicket/TicketReplyBox.stories.tsx +0 -355
  115. package/src/components/supportTicket/TicketReplyBox.tsx +0 -146
  116. package/src/components/supportTicket/fileToAttachment.ts +0 -24
  117. package/src/components/supportTicket/index.ts +0 -8
  118. package/src/components/supportTicket/meta.ts +0 -145
  119. package/src/components/ticketReferences/TicketReferences.stories.tsx +0 -301
  120. package/src/components/ticketReferences/TicketReferences.tsx +0 -899
  121. package/src/components/ticketReferences/index.ts +0 -2
  122. package/src/components/ticketThread/TicketThread.stories.tsx +0 -751
  123. package/src/components/ticketThread/TicketThread.tsx +0 -650
  124. package/src/components/ticketThread/index.ts +0 -6
  125. package/src/helpers/scrollbar.ts +0 -32
@@ -1,751 +0,0 @@
1
- import { StoryObj } from '@storybook/react-vite';
2
- import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
3
- import styled from 'styled-components';
4
-
5
- import { thinScrollbar } from '../../helpers/scrollbar';
6
- import { StoryMeta } from '../../types';
7
- import { ITicketThreadMessage, TicketThread } from './TicketThread';
8
-
9
- /* The drawer body's scroll area — the thin Qorus scrollbar rather than the platform
10
- * default, matching what the app's ThreadScroll should carry. */
11
- const DrawerScroll = styled.div`
12
- flex: 1;
13
- min-height: 0;
14
- overflow-y: auto;
15
- padding: 14px;
16
- ${thinScrollbar}
17
- `;
18
-
19
- /*
20
- * The thread deliberately sits at the BOTTOM of its scroll area, against the composer,
21
- * the way every chat surface behaves. That only reads correctly inside the frame it
22
- * ships in: a fixed-height drawer body with the reply box beneath it. Rendered bare on
23
- * Storybook's full-height canvas the messages look like they fell into a void, with a
24
- * large empty gap above and nothing to sit against.
25
- *
26
- * This decorator supplies that frame — a drawer-height scroll area with the top edge of
27
- * a composer below it — so the bottom-alignment is legible as "a short conversation
28
- * waiting above the reply box" rather than a broken layout. It carries NO text, so the
29
- * `Loading` (asserts empty text) and `Empty` (asserts its own copy) stories are
30
- * unaffected.
31
- */
32
- const DRAWER_HEIGHT = 520;
33
- const inDrawer = (Story: () => JSX.Element) => (
34
- <div
35
- style={{
36
- height: DRAWER_HEIGHT,
37
- display: 'flex',
38
- flexDirection: 'column',
39
- border: '1px solid rgba(255,255,255,0.1)',
40
- borderRadius: 8,
41
- overflow: 'hidden',
42
- }}
43
- >
44
- <DrawerScroll>
45
- <Story />
46
- </DrawerScroll>
47
- {/* the composer's edge — the surface the thread rests against, no text */}
48
- <div
49
- style={{
50
- height: 44,
51
- flex: 'none',
52
- borderTop: '1px solid rgba(255,255,255,0.1)',
53
- background: 'rgba(255,255,255,0.02)',
54
- }}
55
- />
56
- </div>
57
- );
58
-
59
- const NOW = Date.parse('2026-06-30T12:00:00Z');
60
- const ago = (mins: number) => new Date(NOW - mins * 60_000).toISOString();
61
-
62
- /* A stand-in screenshot the mock `fetchAttachmentUrl` serves — an inline SVG data URL,
63
- * so the story needs no network and no fixture file. */
64
- const screenshot = (label: string, hue: number): string =>
65
- `data:image/svg+xml,${encodeURIComponent(
66
- `<svg xmlns='http://www.w3.org/2000/svg' width='320' height='200'>` +
67
- `<rect width='320' height='200' fill='hsl(${hue},38%,16%)'/>` +
68
- `<rect x='16' y='16' width='288' height='30' rx='6' fill='hsl(${hue},45%,30%)'/>` +
69
- `<rect x='16' y='62' width='170' height='12' rx='4' fill='hsl(${hue},30%,46%)'/>` +
70
- `<rect x='16' y='86' width='250' height='12' rx='4' fill='hsl(${hue},24%,38%)'/>` +
71
- `<rect x='16' y='110' width='210' height='12' rx='4' fill='hsl(${hue},24%,38%)'/>` +
72
- `<text x='16' y='180' fill='white' font-family='sans-serif' font-size='13' opacity='0.7'>${label}</text>` +
73
- `</svg>`
74
- )}`;
75
-
76
-
77
- const THREAD: ITicketThreadMessage[] = [
78
- {
79
- message_id: 'm1',
80
- author_type: 'customer',
81
- author_id: 'acme',
82
- body: 'Our nightly **deploy** started failing after the 3.2 upgrade.',
83
- created: ago(180),
84
- attachments: [
85
- {
86
- attachment_id: 'att-1',
87
- filename: 'deploy-trace.log',
88
- content_type: 'text/plain',
89
- size_bytes: 12_800,
90
- },
91
- ],
92
- // no `resolveInterfaceIcon` is passed in these base stories, so this chip
93
- // exercises the built-in default per-kind icon.
94
- referenced_interfaces: [{ interface_kind: 'workflow', interface_name: 'nightly-deploy:1.2' }],
95
- },
96
- {
97
- message_id: 'm2',
98
- author_type: 'staff',
99
- author_id: 'nick',
100
- body: 'Thanks — that trace points at the datasource pool. Can you confirm the pool size?',
101
- created: ago(120),
102
- },
103
- {
104
- message_id: 'm3',
105
- author_type: 'staff',
106
- author_id: 'nick',
107
- body: 'Reproduced locally; the pool default dropped to 1 in 3.2.',
108
- internal: true,
109
- created: ago(90),
110
- },
111
- {
112
- message_id: 'm4',
113
- author_type: 'system',
114
- author_id: 'system',
115
- body: 'Priority raised to **high**.',
116
- created: ago(60),
117
- },
118
- ];
119
-
120
- // A thread whose messages reference interfaces on the customer's instance —
121
- // several at once, including two of the same kind (workflow), plus a job on a
122
- // follow-up message. Referenced interfaces are `{kind, name}` snapshots.
123
- const REFERENCED_THREAD: ITicketThreadMessage[] = [
124
- {
125
- message_id: 'r1',
126
- author_type: 'customer',
127
- author_id: 'acme',
128
- body: 'These two workflows and the notification service all fail together.',
129
- created: ago(45),
130
- referenced_interfaces: [
131
- { interface_kind: 'workflow', interface_name: 'order-processing:1.0', reference_id: 'ref-1' },
132
- { interface_kind: 'workflow', interface_name: 'invoice-sync:2.3', reference_id: 'ref-2' },
133
- { interface_kind: 'service', interface_name: 'notification-svc', reference_id: 'ref-3' },
134
- ],
135
- },
136
- {
137
- message_id: 'r2',
138
- author_type: 'customer',
139
- author_id: 'acme',
140
- body: 'And this job kicks off the follow-up.',
141
- created: ago(20),
142
- referenced_interfaces: [
143
- { interface_kind: 'job', interface_name: 'nightly-cleanup', reference_id: 'ref-4' },
144
- ],
145
- },
146
- ];
147
-
148
- const meta = {
149
- component: TicketThread,
150
- title: 'Components/TicketThread',
151
- decorators: [inDrawer],
152
- args: {
153
- messages: THREAD,
154
- onDownloadAttachment: fn(),
155
- },
156
- } as StoryMeta<typeof TicketThread>;
157
-
158
- export default meta;
159
- type Story = StoryObj<typeof meta>;
160
-
161
- /**
162
- * The customer viewpoint: the customer's own messages sit on the right under
163
- * "You", staff appear under a masked "Qorus Support" label, and internal notes
164
- * are never delivered to this surface (the server filters them). Clicking an
165
- * attachment chip calls `onDownloadAttachment` with its id + filename.
166
- */
167
- export const CustomerView: Story = {
168
- parameters: {
169
- docs: {
170
- description: {
171
- story:
172
- "Renders the thread from the customer's viewpoint: the customer's own messages sit on the right under 'You', staff appear under the masked 'Qorus Support' label, internal notes are filtered out, and clicking an attachment chip calls onDownloadAttachment with its id and filename.",
173
- },
174
- },
175
- },
176
- args: {
177
- viewerRole: 'customer',
178
- // the server never sends internal notes to the customer
179
- messages: THREAD.filter((m) => !m.internal),
180
- },
181
- async play({ canvasElement, args }) {
182
- const canvas = within(canvasElement);
183
-
184
- await expect(canvas.getByText('You')).toBeInTheDocument();
185
- await expect(canvas.getByText('Qorus Support')).toBeInTheDocument();
186
- // internal notes are filtered out for the customer
187
- await expect(canvas.queryByText('Internal note')).not.toBeInTheDocument();
188
-
189
- await userEvent.click(canvas.getAllByText('deploy-trace.log')[0]);
190
- await waitFor(() =>
191
- expect(args.onDownloadAttachment).toHaveBeenCalledWith('att-1', 'deploy-trace.log')
192
- );
193
- },
194
- };
195
-
196
- /**
197
- * Messages can reference interfaces on the customer's instance. Each reference
198
- * renders as a chip under the message — the interface kind as the chip's key and
199
- * the interface name as its label — and several references (including two of the
200
- * same kind) can attach to one message. When the consumer passes
201
- * `onInterfaceClick` (the customer view, which can open the interface on its own
202
- * instance) the chips are clickable; `resolveInterfaceIcon` supplies a per-kind
203
- * icon. The staff view omits `onInterfaceClick`, leaving the chips static.
204
- */
205
- export const WithInterfaceReferences: Story = {
206
- parameters: {
207
- docs: {
208
- description: {
209
- story:
210
- 'Renders a customer thread whose messages carry interface-reference chips — several per message, including two workflows — where clicking a chip calls onInterfaceClick with that interface.',
211
- },
212
- },
213
- },
214
- args: {
215
- viewerRole: 'customer',
216
- messages: REFERENCED_THREAD,
217
- onInterfaceClick: fn(),
218
- resolveInterfaceIcon: (kind) =>
219
- kind === 'workflow' ? 'GitBranchLine' : kind === 'service' ? 'ServerLine' : 'TimerLine',
220
- },
221
- async play({ canvasElement, args }) {
222
- const canvas = within(canvasElement);
223
-
224
- // every reference renders, including two workflows on the same message
225
- await expect(canvas.getByText('order-processing:1.0')).toBeInTheDocument();
226
- await expect(canvas.getByText('invoice-sync:2.3')).toBeInTheDocument();
227
- await expect(canvas.getByText('notification-svc')).toBeInTheDocument();
228
- await expect(canvas.getByText('nightly-cleanup')).toBeInTheDocument();
229
-
230
- // clicking a chip opens that specific interface
231
- await userEvent.click(canvas.getByText('invoice-sync:2.3'));
232
- await waitFor(() =>
233
- expect(args.onInterfaceClick).toHaveBeenCalledWith(
234
- expect.objectContaining({
235
- interface_kind: 'workflow',
236
- interface_name: 'invoice-sync:2.3',
237
- })
238
- )
239
- );
240
- },
241
- };
242
-
243
- /**
244
- * The staff viewpoint: staff messages sit on the right, the customer on the
245
- * left, and staff-only internal notes render amber + bordered under an "Internal
246
- * note" label so they can never be mistaken for a customer-visible reply.
247
- */
248
- export const StaffView: Story = {
249
- parameters: {
250
- docs: {
251
- description: {
252
- story:
253
- "Renders the thread from the staff viewpoint: staff messages sit on the right, the customer on the left, and staff-only internal notes render amber and bordered under an 'Internal note' label.",
254
- },
255
- },
256
- },
257
- args: {
258
- viewerRole: 'staff',
259
- messages: THREAD,
260
- },
261
- async play({ canvasElement }) {
262
- const canvas = within(canvasElement);
263
-
264
- await expect(canvas.getByText('Internal note')).toBeInTheDocument();
265
- await expect(canvas.getByText(/Support · nick/)).toBeInTheDocument();
266
- await expect(canvas.getByText(/Customer · acme/)).toBeInTheDocument();
267
- // the reference chip renders with the built-in default icon (no resolver passed)
268
- await expect(canvas.getByText('nightly-deploy:1.2')).toBeInTheDocument();
269
- },
270
- };
271
-
272
- export const Loading: Story = {
273
- parameters: {
274
- docs: {
275
- description: {
276
- story:
277
- "Renders the loading state as a conversation-shaped skeleton — aria-busy and labelled 'Loading conversation', carrying no message content — rather than a spinner.",
278
- },
279
- },
280
- },
281
- args: { loading: true },
282
- async play({ canvasElement }) {
283
- // the loading state is a conversation-shaped skeleton, not a spinner
284
- const skeleton = canvasElement.querySelector('[aria-busy="true"]');
285
- await expect(skeleton).not.toBeNull();
286
- await expect(skeleton).toHaveAttribute('aria-label', 'Loading conversation');
287
- // no real message content renders while loading
288
- await expect(canvasElement.textContent).toBe('');
289
- },
290
- };
291
-
292
- export const Empty: Story = {
293
- parameters: {
294
- docs: {
295
- description: {
296
- story:
297
- "Renders the empty state with no messages, showing the 'No messages yet' placeholder instead of a thread.",
298
- },
299
- },
300
- },
301
- args: { messages: [] },
302
- async play({ canvasElement }) {
303
- await expect(within(canvasElement).getByText('No messages yet')).toBeInTheDocument();
304
- },
305
- };
306
-
307
- /* ── comprehensive coverage ──────────────────────────────────────────────────
308
- * The stories above cover the two viewpoints and the happy path. The ones below
309
- * exercise the full render surface a message can carry — every interface-reference
310
- * kind, the markdown a body supports, the spread of attachment types, every system
311
- * event, a grouped burst, and the overflow case — so a change to any of those is
312
- * visible in review instead of only showing up against real data.
313
- * ─────────────────────────────────────────────────────────────────────────── */
314
-
315
- /* Every interface kind the reference vocabulary defines, on one message, so each
316
- * chip's built-in per-kind glyph is on screen at once. Names are illustrative. */
317
- const EVERY_KIND: IInterfaceReference[] = [
318
- { interface_kind: 'workflow', interface_name: 'order-sync:1.2' },
319
- { interface_kind: 'service', interface_name: 'notification-svc' },
320
- { interface_kind: 'job', interface_name: 'nightly-recon' },
321
- { interface_kind: 'fsm', interface_name: 'onboarding-flow' },
322
- { interface_kind: 'qog', interface_name: 'stripe-billing' },
323
- { interface_kind: 'connection', interface_name: 'pgsql-omq' },
324
- { interface_kind: 'mapper', interface_name: 'csv-to-order' },
325
- { interface_kind: 'class', interface_name: 'OrderUtils' },
326
- { interface_kind: 'value-map', interface_name: 'country-codes' },
327
- { interface_kind: 'ai-collection', interface_name: 'kb-support' },
328
- { interface_kind: 'ai-endpoint', interface_name: 'gpt-router' },
329
- { interface_kind: 'ai-guardrail', interface_name: 'pii-filter' },
330
- // an unknown kind falls back to the generic glyph rather than breaking
331
- { interface_kind: 'mystery', interface_name: 'future-thing' },
332
- ];
333
-
334
- /** Every referenceable interface kind as a chip on a single message — one of each,
335
- * plus an unknown kind that must fall back to the generic glyph. */
336
- export const EveryReferenceKind: Story = {
337
- parameters: {
338
- docs: {
339
- description: {
340
- story:
341
- 'Renders a single message carrying a chip for every referenceable interface kind, one of each, plus an unknown kind that falls back to the generic glyph.',
342
- },
343
- },
344
- },
345
- args: {
346
- viewerRole: 'customer',
347
- messages: [
348
- {
349
- message_id: 'refs',
350
- author_type: 'customer',
351
- author_id: 'acme',
352
- body: 'For context, this touches most of our instance:',
353
- created: ago(20),
354
- referenced_interfaces: EVERY_KIND,
355
- },
356
- ],
357
- },
358
- async play({ canvasElement }) {
359
- const canvas = within(canvasElement);
360
- for (const reference of EVERY_KIND) {
361
- await expect(canvas.getByText(reference.interface_name)).toBeInTheDocument();
362
- }
363
- },
364
- };
365
-
366
- /** The markdown a body supports: emphasis, inline code, a fenced block, both list
367
- * kinds, and a link — so a formatting regression shows up here, not in the wild. */
368
- export const RichFormatting: Story = {
369
- parameters: {
370
- docs: {
371
- description: {
372
- story:
373
- 'Renders a message whose markdown body exercises emphasis, inline code, a fenced code block, both list kinds, and a link, so a formatting regression surfaces here.',
374
- },
375
- },
376
- },
377
- args: {
378
- viewerRole: 'customer',
379
- messages: [
380
- {
381
- message_id: 'md',
382
- author_type: 'staff',
383
- author_id: 'nick',
384
- created: ago(5),
385
- body: [
386
- 'Here is what I found — the **datasource pool** collapses to `1` under load.',
387
- '',
388
- 'Steps to reproduce:',
389
- '',
390
- '1. Deploy `order-sync:1.2`',
391
- '2. Drive 40+ concurrent orders',
392
- '3. Watch the pool starve',
393
- '',
394
- 'What to check:',
395
- '',
396
- '- the connection `pgsql-omq`',
397
- '- the `max-pool-size` option',
398
- '',
399
- '```qore',
400
- 'int size = pool.getSize();',
401
- 'if (size < 4) log("pool starved: " + size);',
402
- '```',
403
- '',
404
- 'Full write-up in the [runbook](https://docs.qoretechnologies.com).',
405
- ].join('\n'),
406
- },
407
- ],
408
- },
409
- async play({ canvasElement }) {
410
- const canvas = within(canvasElement);
411
- // the fenced block and a list item both render
412
- await expect(canvas.getByText(/pool starved/)).toBeInTheDocument();
413
- await expect(canvas.getByText(/Drive 40\+ concurrent orders/)).toBeInTheDocument();
414
- // the link is a real anchor
415
- const link = canvas.getByText('runbook').closest('a');
416
- await expect(link).toHaveAttribute('href', 'https://docs.qoretechnologies.com');
417
- },
418
- };
419
-
420
- /** A spread of attachment types and sizes on one message — each chip shows its
421
- * own formatted size and a content-type tooltip. */
422
- export const AttachmentTypes: Story = {
423
- parameters: {
424
- docs: {
425
- description: {
426
- story:
427
- 'Renders one message with a spread of attachment types and sizes, where the image previews inline as a thumbnail while the rest stay download chips showing their formatted sizes.',
428
- },
429
- },
430
- },
431
- args: {
432
- viewerRole: 'customer',
433
- // the image among these previews inline; the rest stay download chips
434
- fetchAttachmentUrl: async () => screenshot('screenshot', 210),
435
- messages: [
436
- {
437
- message_id: 'att',
438
- author_type: 'customer',
439
- author_id: 'acme',
440
- body: 'Attaching everything I gathered:',
441
- created: ago(8),
442
- attachments: [
443
- { attachment_id: 'a1', filename: 'screenshot.png', content_type: 'image/png', size_bytes: 842_000 },
444
- { attachment_id: 'a2', filename: 'deploy-trace.log', content_type: 'text/plain', size_bytes: 12_800 },
445
- { attachment_id: 'a3', filename: 'incident-report.pdf', content_type: 'application/pdf', size_bytes: 3_400_000 },
446
- { attachment_id: 'a4', filename: 'pool-config.yaml', content_type: 'application/x-yaml', size_bytes: 640 },
447
- { attachment_id: 'a5', filename: 'heap-dump.hprof', content_type: 'application/octet-stream', size_bytes: 1_750_000_000 },
448
- ],
449
- },
450
- ],
451
- },
452
- async play({ canvasElement }) {
453
- const canvas = within(canvasElement);
454
- // the image renders as a thumbnail (an <img>, not a filename chip) …
455
- await waitFor(() => {
456
- const img = canvasElement.querySelector('img[alt="screenshot.png"]');
457
- if (!img) {
458
- throw new Error('image thumbnail not rendered yet');
459
- }
460
- });
461
- // … while the non-image files stay chips, each with its per-unit size
462
- await expect(canvas.getByText('deploy-trace.log')).toBeInTheDocument();
463
- await expect(canvas.getByText('640 B')).toBeInTheDocument();
464
- await expect(canvas.getByText('1.6 GB')).toBeInTheDocument();
465
- },
466
- };
467
-
468
- /** The system-message range — created, assigned, priority, status, reopened,
469
- * resolved — each a centred muted line rather than a card. */
470
- export const SystemEvents: Story = {
471
- parameters: {
472
- docs: {
473
- description: {
474
- story:
475
- 'Renders the full range of system messages — opened, assigned, priority, status, reopened, resolved — each as a centred muted line rather than a card.',
476
- },
477
- },
478
- },
479
- args: {
480
- viewerRole: 'customer',
481
- messages: [
482
- { message_id: 's1', author_type: 'system', author_id: 'system', body: 'Ticket opened.', created: ago(300) },
483
- { message_id: 's2', author_type: 'system', author_id: 'system', body: 'Assigned to Qorus Support.', created: ago(280) },
484
- { message_id: 'c1', author_type: 'customer', author_id: 'acme', body: 'Any update on this?', created: ago(200) },
485
- { message_id: 's3', author_type: 'system', author_id: 'system', body: 'Priority raised to **High**.', created: ago(190) },
486
- { message_id: 's4', author_type: 'system', author_id: 'system', body: 'Status changed to **Awaiting your reply**.', created: ago(120) },
487
- { message_id: 's5', author_type: 'system', author_id: 'system', body: 'Ticket reopened.', created: ago(60) },
488
- { message_id: 's6', author_type: 'system', author_id: 'system', body: 'Ticket resolved.', created: ago(5) },
489
- ],
490
- },
491
- async play({ canvasElement }) {
492
- const canvas = within(canvasElement);
493
- await expect(canvas.getByText(/Ticket opened/)).toBeInTheDocument();
494
- await expect(canvas.getByText(/Ticket resolved/)).toBeInTheDocument();
495
- },
496
- };
497
-
498
- /** A burst of consecutive same-author replies collapses under a single timestamp
499
- * (one `ReqoreBubbleGroup`) rather than stamping every card. */
500
- export const GroupedBurst: Story = {
501
- parameters: {
502
- docs: {
503
- description: {
504
- story:
505
- 'Renders a burst of consecutive same-author staff replies collapsed under a single timestamp, rather than stamping every card.',
506
- },
507
- },
508
- },
509
- args: {
510
- viewerRole: 'staff',
511
- messages: [
512
- { message_id: 'g0', author_type: 'customer', author_id: 'acme', body: 'The deploy is still failing.', created: ago(30) },
513
- { message_id: 'g1', author_type: 'staff', author_id: 'nick', body: 'Looking now.', created: ago(9) },
514
- { message_id: 'g2', author_type: 'staff', author_id: 'nick', body: 'Reproduced it locally.', created: ago(8) },
515
- { message_id: 'g3', author_type: 'staff', author_id: 'nick', body: 'The pool default dropped to 1 in 3.2.', created: ago(7) },
516
- { message_id: 'g4', author_type: 'staff', author_id: 'nick', body: 'Fix is going out in 3.2.1 — I will confirm here.', created: ago(6) },
517
- ],
518
- },
519
- };
520
-
521
- /** An internal note sits amber under "Internal note" for staff, next to a normal
522
- * reply — the two must never be confusable. Customer never receives it. */
523
- export const InternalNote: Story = {
524
- parameters: {
525
- docs: {
526
- description: {
527
- story:
528
- "Renders a staff thread where an internal note sits amber under 'Internal note' beside a normal reply, so the two can never be confused.",
529
- },
530
- },
531
- },
532
- args: {
533
- viewerRole: 'staff',
534
- messages: [
535
- { message_id: 'i0', author_type: 'customer', author_id: 'acme', body: 'Is there a workaround while I wait?', created: ago(40) },
536
- {
537
- message_id: 'i1',
538
- author_type: 'staff',
539
- author_id: 'nick',
540
- internal: true,
541
- body: 'Root cause is the 3.2 pool default. Do **not** promise a date yet — patch is unverified.',
542
- created: ago(30),
543
- },
544
- { message_id: 'i2', author_type: 'staff', author_id: 'nick', body: 'You can raise `max-pool-size` to 8 as a temporary workaround.', created: ago(20) },
545
- ],
546
- },
547
- async play({ canvasElement }) {
548
- const canvas = within(canvasElement);
549
- await expect(canvas.getByText('Internal note')).toBeInTheDocument();
550
- },
551
- };
552
-
553
- /** A single message — the footer count reads "1 message", not "1 messages". */
554
- export const SingleMessage: Story = {
555
- parameters: {
556
- docs: {
557
- description: {
558
- story:
559
- "Renders a thread with a single message, so the footer count reads '1 message' rather than '1 messages'.",
560
- },
561
- },
562
- },
563
- args: {
564
- viewerRole: 'customer',
565
- messages: [
566
- { message_id: 'one', author_type: 'customer', author_id: 'acme', body: 'Just opened this — nothing else yet.', created: ago(2) },
567
- ],
568
- },
569
- async play({ canvasElement }) {
570
- await expect(within(canvasElement).getByText('1 message')).toBeInTheDocument();
571
- },
572
- };
573
-
574
- /** A long conversation that overflows the drawer body — confirms it scrolls and
575
- * the latest message sits at the bottom against the composer. */
576
- export const LongThread: Story = {
577
- parameters: {
578
- docs: {
579
- description: {
580
- story:
581
- 'Renders a 16-message conversation that overflows the drawer body, confirming it scrolls and auto-scrolls to the newest message resting at the bottom against the composer.',
582
- },
583
- },
584
- },
585
- args: {
586
- viewerRole: 'customer',
587
- messages: Array.from({ length: 16 }, (_, i) => ({
588
- message_id: `l${i}`,
589
- author_type: (i % 2 === 0 ? 'customer' : 'staff') as TTicketMessageAuthor,
590
- author_id: i % 2 === 0 ? 'acme' : 'nick',
591
- body:
592
- i % 2 === 0
593
- ? `Follow-up ${i / 2 + 1}: still seeing the failure after the last change.`
594
- : 'Thanks — pushing another fix and will report back.',
595
- created: ago((16 - i) * 15),
596
- })),
597
- },
598
- async play({ canvasElement }) {
599
- const canvas = within(canvasElement);
600
- const footer = await canvas.findByText('16 messages');
601
- // The 16-message thread overflows the 520px drawer body — walk up to the scroll area.
602
- let scroller = footer.parentElement;
603
- while (scroller && scroller.scrollHeight <= scroller.clientHeight) {
604
- scroller = scroller.parentElement;
605
- }
606
- if (!scroller) {
607
- throw new Error('no scroll container around the overflowing thread');
608
- }
609
- const scrollArea = scroller;
610
- // On open the thread auto-scrolls to the newest message: the area rests at its END,
611
- // not at the top (scrollTop 0) on the oldest message. Drop the scroll effect in
612
- // TicketThread and scrollTop stays 0 — this assertion then fails.
613
- await waitFor(() => {
614
- expect(scrollArea.scrollTop).toBeGreaterThan(0);
615
- expect(scrollArea.scrollTop + scrollArea.clientHeight).toBeGreaterThanOrEqual(
616
- scrollArea.scrollHeight - 4
617
- );
618
- });
619
- },
620
- };
621
-
622
- /**
623
- * Image attachments preview inline: a screenshot renders as a thumbnail the reader can
624
- * click to enlarge (`fetchAttachmentUrl` supplies the bytes), while a non-image on the
625
- * same message stays a download chip. Without `fetchAttachmentUrl` every attachment —
626
- * images included — falls back to the chip, which is what `CustomerView` shows.
627
- */
628
- export const ImagePreview: Story = {
629
- parameters: {
630
- docs: {
631
- description: {
632
- story:
633
- 'Renders a message whose image attachments preview inline as thumbnails — clicking one opens the enlarge view — while a non-image on the same message stays a download chip.',
634
- },
635
- },
636
- },
637
- args: {
638
- viewerRole: 'customer',
639
- fetchAttachmentUrl: async (id: string) =>
640
- screenshot(id === 'img-1' ? 'before' : 'after', id === 'img-1' ? 265 : 205),
641
- messages: [
642
- {
643
- message_id: 'img',
644
- author_type: 'customer',
645
- author_id: 'acme',
646
- body: 'Here is the canvas before and after the upgrade, plus the server log:',
647
- created: ago(6),
648
- attachments: [
649
- { attachment_id: 'img-1', filename: 'before.png', content_type: 'image/png', size_bytes: 512_000 },
650
- { attachment_id: 'img-2', filename: 'after.png', content_type: 'image/png', size_bytes: 486_000 },
651
- { attachment_id: 'log-1', filename: 'server.log', content_type: 'text/plain', size_bytes: 24_800 },
652
- ],
653
- },
654
- ],
655
- },
656
- async play({ canvasElement }) {
657
- const canvas = within(canvasElement);
658
- // the two images render as thumbnails …
659
- const thumb = await waitFor(() => {
660
- const img = canvasElement.querySelector<HTMLImageElement>('img[alt="before.png"]');
661
- if (!img) {
662
- throw new Error('thumbnail not rendered yet');
663
- }
664
- return img;
665
- });
666
- // … while the non-image stays a download chip
667
- await expect(canvas.getByText('server.log')).toBeInTheDocument();
668
- // clicking a thumbnail opens the enlarge view
669
- await userEvent.click(thumb);
670
- await waitFor(() => expect(document.querySelector('[role="dialog"]')).not.toBeNull());
671
- },
672
- };
673
-
674
- /**
675
- * One message carrying the whole stack at once — a formatted body, image thumbnails,
676
- * file chips, and interface references — so the vertical order and spacing of all four
677
- * blocks together is reviewable in one place, not only in isolation. Body first, then
678
- * the two attachment rows (images, then files), then the reference chips.
679
- */
680
- export const Everything: Story = {
681
- parameters: {
682
- docs: {
683
- description: {
684
- story:
685
- 'Renders one message carrying the whole stack at once — a formatted body, image thumbnails, file chips, and interface references — so the order and spacing of all four blocks is reviewable together.',
686
- },
687
- },
688
- },
689
- args: {
690
- viewerRole: 'customer',
691
- fetchAttachmentUrl: async (id: string) =>
692
- screenshot(id === 'e-img-1' ? 'canvas' : 'console', id === 'e-img-1' ? 265 : 200),
693
- resolveInterfaceIcon: (kind) =>
694
- kind === 'workflow'
695
- ? 'GitBranchLine'
696
- : kind === 'service'
697
- ? 'ServerLine'
698
- : kind === 'job'
699
- ? 'CalendarLine'
700
- : 'Plug2Line',
701
- onInterfaceClick: fn(),
702
- messages: [
703
- {
704
- message_id: 'everything',
705
- author_type: 'customer',
706
- author_id: 'acme',
707
- created: ago(4),
708
- body: [
709
- 'The nightly **deploy** fails at the mapper step. Two screenshots, the trace, the',
710
- 'config, and the interfaces involved:',
711
- '',
712
- '- reproduces every run since `3.2`',
713
- '- only when the pool drops below `4`',
714
- ].join('\n'),
715
- attachments: [
716
- { attachment_id: 'e-img-1', filename: 'canvas-error.png', content_type: 'image/png', size_bytes: 512_000 },
717
- { attachment_id: 'e-img-2', filename: 'console.png', content_type: 'image/png', size_bytes: 388_000 },
718
- { attachment_id: 'e-file-1', filename: 'deploy-trace.log', content_type: 'text/plain', size_bytes: 12_800 },
719
- { attachment_id: 'e-file-2', filename: 'pool-config.yaml', content_type: 'application/x-yaml', size_bytes: 640 },
720
- ],
721
- referenced_interfaces: [
722
- { interface_kind: 'workflow', interface_name: 'order-sync:1.2', reference_id: 'e-ref-1' },
723
- { interface_kind: 'service', interface_name: 'notification-svc', reference_id: 'e-ref-2' },
724
- { interface_kind: 'job', interface_name: 'nightly-recon', reference_id: 'e-ref-3' },
725
- { interface_kind: 'connection', interface_name: 'pgsql-omq', reference_id: 'e-ref-4' },
726
- ],
727
- },
728
- {
729
- message_id: 'everything-reply',
730
- author_type: 'staff',
731
- author_id: 'nick',
732
- created: ago(1),
733
- body: 'Got it — the screenshots confirm the mapper NPE. Looking at the pool default now.',
734
- },
735
- ],
736
- },
737
- async play({ canvasElement }) {
738
- const canvas = within(canvasElement);
739
- // an image thumbnail …
740
- await waitFor(() => {
741
- if (!canvasElement.querySelector('img[alt="canvas-error.png"]')) {
742
- throw new Error('thumbnail not rendered yet');
743
- }
744
- });
745
- // … a file chip …
746
- await expect(canvas.getByText('deploy-trace.log')).toBeInTheDocument();
747
- // … and an interface reference, all on the one message
748
- await expect(canvas.getByText('order-sync:1.2')).toBeInTheDocument();
749
- await expect(canvas.getByText('pgsql-omq')).toBeInTheDocument();
750
- },
751
- };