@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.
- package/.claude/CLAUDE.md +8 -11
- package/dist/components/form/engine/FormEngine.d.ts +1 -1
- package/dist/helpers/common.d.ts +0 -5
- package/dist/helpers/common.d.ts.map +1 -1
- package/dist/helpers/common.js +1 -20
- package/dist/helpers/common.js.map +1 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/dist/providers/StorageProvider.d.ts +13 -0
- package/dist/providers/StorageProvider.d.ts.map +1 -1
- package/dist/providers/StorageProvider.js +29 -5
- package/dist/providers/StorageProvider.js.map +1 -1
- package/package.json +3 -3
- package/src/helpers/common.ts +0 -19
- package/src/hooks/useStorage/useStorage.stories.tsx +52 -0
- package/src/index.tsx +0 -5
- package/src/providers/StorageProvider.tsx +30 -4
- package/dist/components/composer/Composer.d.ts +0 -108
- package/dist/components/composer/Composer.d.ts.map +0 -1
- package/dist/components/composer/Composer.js +0 -238
- package/dist/components/composer/Composer.js.map +0 -1
- package/dist/components/composer/helpers.d.ts +0 -21
- package/dist/components/composer/helpers.d.ts.map +0 -1
- package/dist/components/composer/helpers.js +0 -75
- package/dist/components/composer/helpers.js.map +0 -1
- package/dist/components/composer/index.d.ts +0 -3
- package/dist/components/composer/index.d.ts.map +0 -1
- package/dist/components/composer/index.js +0 -19
- package/dist/components/composer/index.js.map +0 -1
- package/dist/components/imageLightbox/ImageLightbox.d.ts +0 -17
- package/dist/components/imageLightbox/ImageLightbox.d.ts.map +0 -1
- package/dist/components/imageLightbox/ImageLightbox.js +0 -129
- package/dist/components/imageLightbox/ImageLightbox.js.map +0 -1
- package/dist/components/imageLightbox/index.d.ts +0 -2
- package/dist/components/imageLightbox/index.d.ts.map +0 -1
- package/dist/components/imageLightbox/index.js +0 -18
- package/dist/components/imageLightbox/index.js.map +0 -1
- package/dist/components/supportTicket/AttachmentChips.d.ts +0 -10
- package/dist/components/supportTicket/AttachmentChips.d.ts.map +0 -1
- package/dist/components/supportTicket/AttachmentChips.js +0 -119
- package/dist/components/supportTicket/AttachmentChips.js.map +0 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +0 -35
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +0 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.js +0 -31
- package/dist/components/supportTicket/InterfaceReferenceTags.js.map +0 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts +0 -41
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +0 -1
- package/dist/components/supportTicket/ReferencePicker.js +0 -86
- package/dist/components/supportTicket/ReferencePicker.js.map +0 -1
- package/dist/components/supportTicket/TicketHeader.d.ts +0 -86
- package/dist/components/supportTicket/TicketHeader.d.ts.map +0 -1
- package/dist/components/supportTicket/TicketHeader.js +0 -149
- package/dist/components/supportTicket/TicketHeader.js.map +0 -1
- package/dist/components/supportTicket/TicketMetaTags.d.ts +0 -32
- package/dist/components/supportTicket/TicketMetaTags.d.ts.map +0 -1
- package/dist/components/supportTicket/TicketMetaTags.js +0 -21
- package/dist/components/supportTicket/TicketMetaTags.js.map +0 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts +0 -63
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +0 -1
- package/dist/components/supportTicket/TicketReplyBox.js +0 -115
- package/dist/components/supportTicket/TicketReplyBox.js.map +0 -1
- package/dist/components/supportTicket/fileToAttachment.d.ts +0 -9
- package/dist/components/supportTicket/fileToAttachment.d.ts.map +0 -1
- package/dist/components/supportTicket/fileToAttachment.js +0 -28
- package/dist/components/supportTicket/fileToAttachment.js.map +0 -1
- package/dist/components/supportTicket/index.d.ts +0 -9
- package/dist/components/supportTicket/index.d.ts.map +0 -1
- package/dist/components/supportTicket/index.js +0 -25
- package/dist/components/supportTicket/index.js.map +0 -1
- package/dist/components/supportTicket/meta.d.ts +0 -63
- package/dist/components/supportTicket/meta.d.ts.map +0 -1
- package/dist/components/supportTicket/meta.js +0 -79
- package/dist/components/supportTicket/meta.js.map +0 -1
- package/dist/components/ticketReferences/TicketReferences.d.ts +0 -52
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +0 -1
- package/dist/components/ticketReferences/TicketReferences.js +0 -453
- package/dist/components/ticketReferences/TicketReferences.js.map +0 -1
- package/dist/components/ticketReferences/index.d.ts +0 -3
- package/dist/components/ticketReferences/index.d.ts.map +0 -1
- package/dist/components/ticketReferences/index.js +0 -6
- package/dist/components/ticketReferences/index.js.map +0 -1
- package/dist/components/ticketThread/TicketThread.d.ts +0 -52
- package/dist/components/ticketThread/TicketThread.d.ts.map +0 -1
- package/dist/components/ticketThread/TicketThread.js +0 -248
- package/dist/components/ticketThread/TicketThread.js.map +0 -1
- package/dist/components/ticketThread/index.d.ts +0 -3
- package/dist/components/ticketThread/index.d.ts.map +0 -1
- package/dist/components/ticketThread/index.js +0 -7
- package/dist/components/ticketThread/index.js.map +0 -1
- package/dist/helpers/scrollbar.d.ts +0 -2
- package/dist/helpers/scrollbar.d.ts.map +0 -1
- package/dist/helpers/scrollbar.js +0 -25
- package/dist/helpers/scrollbar.js.map +0 -1
- package/src/components/composer/Composer.stories.tsx +0 -238
- package/src/components/composer/Composer.tsx +0 -545
- package/src/components/composer/helpers.ts +0 -74
- package/src/components/composer/index.ts +0 -2
- package/src/components/imageLightbox/ImageLightbox.stories.tsx +0 -156
- package/src/components/imageLightbox/ImageLightbox.tsx +0 -169
- package/src/components/imageLightbox/index.ts +0 -1
- package/src/components/supportTicket/AttachmentChips.stories.tsx +0 -122
- package/src/components/supportTicket/AttachmentChips.tsx +0 -142
- package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +0 -134
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +0 -85
- package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -296
- package/src/components/supportTicket/ReferencePicker.tsx +0 -237
- package/src/components/supportTicket/TicketConversation.stories.tsx +0 -122
- package/src/components/supportTicket/TicketHeader.stories.tsx +0 -183
- package/src/components/supportTicket/TicketHeader.tsx +0 -247
- package/src/components/supportTicket/TicketMetaTags.stories.tsx +0 -103
- package/src/components/supportTicket/TicketMetaTags.tsx +0 -85
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +0 -355
- package/src/components/supportTicket/TicketReplyBox.tsx +0 -146
- package/src/components/supportTicket/fileToAttachment.ts +0 -24
- package/src/components/supportTicket/index.ts +0 -8
- package/src/components/supportTicket/meta.ts +0 -145
- package/src/components/ticketReferences/TicketReferences.stories.tsx +0 -301
- package/src/components/ticketReferences/TicketReferences.tsx +0 -899
- package/src/components/ticketReferences/index.ts +0 -2
- package/src/components/ticketThread/TicketThread.stories.tsx +0 -751
- package/src/components/ticketThread/TicketThread.tsx +0 -650
- package/src/components/ticketThread/index.ts +0 -6
- package/src/helpers/scrollbar.ts +0 -32
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { ReqoreInput, ReqoreMenu, ReqoreMenuItem, ReqoreSpinner } from '@qoretechnologies/reqore';
|
|
2
|
-
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
3
|
-
import { ReactNode, useMemo, useState } from 'react';
|
|
4
|
-
import styled from 'styled-components';
|
|
5
|
-
import { thinScrollbar } from '../../helpers/scrollbar';
|
|
6
|
-
import { defaultInterfaceIcon, IInterfaceReference } from './meta';
|
|
7
|
-
|
|
8
|
-
/*
|
|
9
|
-
* The two-pane interface browser: kinds down the left, the selected kind's interfaces
|
|
10
|
-
* on the right, and one search field that scopes both. Sits above a ticket composer,
|
|
11
|
-
* toggled by the composer's "Reference interfaces" action.
|
|
12
|
-
*
|
|
13
|
-
* The host owns the data. Which kinds exist and what lives under one are questions only
|
|
14
|
-
* the surrounding app can answer — the IDE lists them off the customer's live instance,
|
|
15
|
-
* a story hands over fixtures — so this component takes `kinds` and the selected kind's
|
|
16
|
-
* `items` and does the rest: selection, search, and turning a click into a reference.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** How many interfaces the right pane shows before it scrolls. */
|
|
20
|
-
const PANE_MAX_HEIGHT = 180;
|
|
21
|
-
const KIND_PANE_WIDTH = 150;
|
|
22
|
-
|
|
23
|
-
/*
|
|
24
|
-
* A scroll pane with the thin, unobtrusive scrollbar the Qonsole surfaces use
|
|
25
|
-
* (transparent track, translucent rounded thumb) rather than the raw browser one.
|
|
26
|
-
* webkit-only, like those surfaces: setting `scrollbar-width` as well would let
|
|
27
|
-
* Chromium override this custom 6px with its own, wider, "thin".
|
|
28
|
-
*/
|
|
29
|
-
const Pane = styled.div<{ $right?: boolean }>`
|
|
30
|
-
${({ $right }) =>
|
|
31
|
-
$right
|
|
32
|
-
? 'flex: 1; min-width: 0;'
|
|
33
|
-
: `width: ${KIND_PANE_WIDTH}px; flex: none; border-right: 1px solid rgba(123, 104, 238, 0.25);`}
|
|
34
|
-
max-height: ${PANE_MAX_HEIGHT}px;
|
|
35
|
-
overflow-y: auto;
|
|
36
|
-
padding: 4px;
|
|
37
|
-
|
|
38
|
-
${thinScrollbar}
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
const Frame = styled.div`
|
|
42
|
-
border: 1px solid rgba(123, 104, 238, 0.25);
|
|
43
|
-
border-radius: 8px;
|
|
44
|
-
overflow: hidden;
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
const Panes = styled.div`
|
|
48
|
-
display: flex;
|
|
49
|
-
`;
|
|
50
|
-
|
|
51
|
-
/*
|
|
52
|
-
* An underline search field. The input itself is borderless (`flat transparent`) and
|
|
53
|
-
* this wrapper carries the single bottom border, so the search reads as a line rather
|
|
54
|
-
* than a full grey box — including on focus, where Reqore's default is a 2px outline
|
|
55
|
-
* around the whole control. Suppressing that outline and brightening the underline
|
|
56
|
-
* instead keeps a focus cue without the box. The selector is scoped under this wrapper,
|
|
57
|
-
* so it outranks Reqore's without `!important`.
|
|
58
|
-
*/
|
|
59
|
-
const Search = styled.div<{ $accent: string }>`
|
|
60
|
-
padding: 6px 4px 4px;
|
|
61
|
-
border-bottom: 1px solid ${({ $accent }) => `${$accent}80`};
|
|
62
|
-
transition: border-color 0.15s ease-out;
|
|
63
|
-
|
|
64
|
-
.reqore-control-wrapper:focus-within {
|
|
65
|
-
outline: none;
|
|
66
|
-
}
|
|
67
|
-
&:focus-within {
|
|
68
|
-
border-bottom-color: ${({ $accent }) => $accent};
|
|
69
|
-
}
|
|
70
|
-
`;
|
|
71
|
-
|
|
72
|
-
const Note = styled.div`
|
|
73
|
-
opacity: 0.4;
|
|
74
|
-
padding: 8px 6px;
|
|
75
|
-
font-size: 12px;
|
|
76
|
-
`;
|
|
77
|
-
|
|
78
|
-
const DEFAULT_ACCENT = '#762f7e';
|
|
79
|
-
|
|
80
|
-
export interface IReferencePickerProps {
|
|
81
|
-
/** the interface kinds listed down the left pane */
|
|
82
|
-
kinds: string[];
|
|
83
|
-
/** the kind whose interfaces `items` holds */
|
|
84
|
-
kind: string;
|
|
85
|
-
onKindChange: (kind: string) => void;
|
|
86
|
-
/**
|
|
87
|
-
* The selected kind's interfaces, by name. Fetching is the host's job — it knows
|
|
88
|
-
* where they come from and how to cache them.
|
|
89
|
-
*/
|
|
90
|
-
items: string[];
|
|
91
|
-
/** `items` is still being fetched, so the right pane shows a spinner rather than
|
|
92
|
-
* claiming the kind is empty */
|
|
93
|
-
loading?: boolean;
|
|
94
|
-
/**
|
|
95
|
-
* The host's fetch for this kind failed. Shown in place of the list, because an
|
|
96
|
-
* empty pane would otherwise read as "this instance has no workflows" — which is a
|
|
97
|
-
* different and much more alarming statement than "we couldn't ask".
|
|
98
|
-
*/
|
|
99
|
-
error?: string;
|
|
100
|
-
/** Retry the failed fetch. The error state is pressable when given. */
|
|
101
|
-
onRetry?: () => void;
|
|
102
|
-
/** Already-picked references. Anything of the current kind that is already picked is
|
|
103
|
-
* dropped from the right pane, so a reference can't be added twice. */
|
|
104
|
-
picked?: IInterfaceReference[];
|
|
105
|
-
onAdd: (reference: IInterfaceReference) => void;
|
|
106
|
-
/** the whole picker is inert (e.g. a reply is in flight) */
|
|
107
|
-
disabled?: boolean;
|
|
108
|
-
/** Per-kind icon. Consumers with their own icon vocabulary (the IDE) pass one; the
|
|
109
|
-
* built-in per-kind default is used when omitted. */
|
|
110
|
-
resolveInterfaceIcon?: (kind: string) => IReqoreIconName;
|
|
111
|
-
/** the underline / selected-kind colour. Defaults to the support surfaces' violet. */
|
|
112
|
-
accent?: string;
|
|
113
|
-
/** shown above the panes — the composer's picked-reference chips, typically */
|
|
114
|
-
header?: ReactNode;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export const ReferencePicker = ({
|
|
118
|
-
kinds,
|
|
119
|
-
kind,
|
|
120
|
-
onKindChange,
|
|
121
|
-
items,
|
|
122
|
-
loading,
|
|
123
|
-
error,
|
|
124
|
-
onRetry,
|
|
125
|
-
picked,
|
|
126
|
-
onAdd,
|
|
127
|
-
disabled,
|
|
128
|
-
resolveInterfaceIcon = defaultInterfaceIcon,
|
|
129
|
-
accent = DEFAULT_ACCENT,
|
|
130
|
-
header,
|
|
131
|
-
}: IReferencePickerProps) => {
|
|
132
|
-
const [query, setQuery] = useState<string>('');
|
|
133
|
-
const needle = query.trim().toLowerCase();
|
|
134
|
-
|
|
135
|
-
const visibleKinds = useMemo(
|
|
136
|
-
() => (needle ? kinds.filter((k) => k.toLowerCase().includes(needle)) : kinds),
|
|
137
|
-
[kinds, needle]
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
/*
|
|
141
|
-
* The right pane drops what's already been picked and then applies the search. When
|
|
142
|
-
* the KIND itself matches the search ("workflow"), its interfaces all stay — you
|
|
143
|
-
* searched for the kind, so you want to see what's in it.
|
|
144
|
-
*/
|
|
145
|
-
const visibleItems = useMemo(() => {
|
|
146
|
-
const already = new Set(
|
|
147
|
-
(picked ?? [])
|
|
148
|
-
.filter((reference) => reference.interface_kind === kind)
|
|
149
|
-
.map((reference) => reference.interface_name)
|
|
150
|
-
);
|
|
151
|
-
const kindMatches = kind.toLowerCase().includes(needle);
|
|
152
|
-
return items.filter(
|
|
153
|
-
(name) =>
|
|
154
|
-
!already.has(name) && (!needle || kindMatches || name.toLowerCase().includes(needle))
|
|
155
|
-
);
|
|
156
|
-
}, [items, picked, kind, needle]);
|
|
157
|
-
|
|
158
|
-
return (
|
|
159
|
-
<Frame>
|
|
160
|
-
{header}
|
|
161
|
-
<Search $accent={accent}>
|
|
162
|
-
<ReqoreInput
|
|
163
|
-
value={query}
|
|
164
|
-
onChange={(event) => setQuery((event.target as HTMLInputElement).value)}
|
|
165
|
-
placeholder='Search kinds and interfaces…'
|
|
166
|
-
icon='SearchLine'
|
|
167
|
-
minimal
|
|
168
|
-
flat
|
|
169
|
-
transparent
|
|
170
|
-
fluid
|
|
171
|
-
disabled={disabled}
|
|
172
|
-
onClearClick={query ? () => setQuery('') : undefined}
|
|
173
|
-
/>
|
|
174
|
-
</Search>
|
|
175
|
-
<Panes>
|
|
176
|
-
{/* Both panes are `ReqoreMenu`s of `ReqoreMenuItem`s — a vertical selectable
|
|
177
|
-
list is exactly what Menu is, so the selection (`selected`), the left-icon +
|
|
178
|
-
right-icon, and the item chrome come from reqore rather than hand-styled
|
|
179
|
-
buttons. The Pane wrapper still owns the scroll (fixed/flex width +
|
|
180
|
-
thinScrollbar); the menus render transparent inside it. */}
|
|
181
|
-
<Pane>
|
|
182
|
-
{visibleKinds.length ? (
|
|
183
|
-
<ReqoreMenu transparent flat padded={false} width='100%' size='small'>
|
|
184
|
-
{visibleKinds.map((option) => (
|
|
185
|
-
<ReqoreMenuItem
|
|
186
|
-
key={option}
|
|
187
|
-
icon={resolveInterfaceIcon(option)}
|
|
188
|
-
label={option}
|
|
189
|
-
selected={option === kind}
|
|
190
|
-
disabled={disabled}
|
|
191
|
-
onClick={() => onKindChange(option)}
|
|
192
|
-
/>
|
|
193
|
-
))}
|
|
194
|
-
</ReqoreMenu>
|
|
195
|
-
) : (
|
|
196
|
-
<Note>No kinds</Note>
|
|
197
|
-
)}
|
|
198
|
-
</Pane>
|
|
199
|
-
<Pane $right>
|
|
200
|
-
{error ? (
|
|
201
|
-
<ReqoreMenu transparent flat padded={false} width='100%' size='small'>
|
|
202
|
-
<ReqoreMenuItem
|
|
203
|
-
icon='ErrorWarningLine'
|
|
204
|
-
rightIcon={onRetry ? 'RefreshLine' : undefined}
|
|
205
|
-
intent='danger'
|
|
206
|
-
label={`Couldn't load ${kind}s`}
|
|
207
|
-
/* the specific failure is the tooltip — the pane states the outcome */
|
|
208
|
-
tooltip={error}
|
|
209
|
-
onClick={onRetry}
|
|
210
|
-
/>
|
|
211
|
-
</ReqoreMenu>
|
|
212
|
-
) : loading ? (
|
|
213
|
-
<ReqoreSpinner iconColor='info' size='small' centered>
|
|
214
|
-
Loading {kind}s…
|
|
215
|
-
</ReqoreSpinner>
|
|
216
|
-
) : visibleItems.length ? (
|
|
217
|
-
<ReqoreMenu transparent flat padded={false} width='100%' size='small'>
|
|
218
|
-
{visibleItems.map((name) => (
|
|
219
|
-
<ReqoreMenuItem
|
|
220
|
-
key={name}
|
|
221
|
-
icon={resolveInterfaceIcon(kind)}
|
|
222
|
-
rightIcon='AddLine'
|
|
223
|
-
label={name}
|
|
224
|
-
disabled={disabled}
|
|
225
|
-
tooltip={`Reference ${kind} ${name}`}
|
|
226
|
-
onClick={() => onAdd({ interface_kind: kind, interface_name: name })}
|
|
227
|
-
/>
|
|
228
|
-
))}
|
|
229
|
-
</ReqoreMenu>
|
|
230
|
-
) : (
|
|
231
|
-
<Note>No matching {kind}</Note>
|
|
232
|
-
)}
|
|
233
|
-
</Pane>
|
|
234
|
-
</Panes>
|
|
235
|
-
</Frame>
|
|
236
|
-
);
|
|
237
|
-
};
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { ReqoreUIProvider } from '@qoretechnologies/reqore';
|
|
2
|
-
import { StoryObj } from '@storybook/react-vite';
|
|
3
|
-
import { expect, fn } from 'storybook/test';
|
|
4
|
-
import { ReactNode } from 'react';
|
|
5
|
-
import { StoryMeta } from '../../types';
|
|
6
|
-
import { ITicketThreadMessage, TicketThread } from '../ticketThread/TicketThread';
|
|
7
|
-
import { InterfaceReferenceTags } from './InterfaceReferenceTags';
|
|
8
|
-
import { TicketReplyBox } from './TicketReplyBox';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The full ticket surface — the shared `<TicketThread>` conversation above the
|
|
12
|
-
* `<TicketReplyBox>` composer, the way a ticket detail (customer) or the staff
|
|
13
|
-
* drawer stacks them. Shown under the Qorus theme so the `custom1` accent renders.
|
|
14
|
-
*/
|
|
15
|
-
const meta = {
|
|
16
|
-
component: TicketReplyBox,
|
|
17
|
-
title: 'Components/TicketConversation',
|
|
18
|
-
args: { onSend: fn() },
|
|
19
|
-
} as StoryMeta<typeof TicketReplyBox>;
|
|
20
|
-
|
|
21
|
-
export default meta;
|
|
22
|
-
type Story = StoryObj<typeof meta>;
|
|
23
|
-
|
|
24
|
-
const CUSTOMER: ITicketThreadMessage = {
|
|
25
|
-
message_id: 'm1',
|
|
26
|
-
author_type: 'customer',
|
|
27
|
-
author_id: 'ops@acme.io',
|
|
28
|
-
body: 'Since upgrading to 3.2 our nightly SFTP transfer to the partner endpoint times out after about 30 seconds. It ran fine on 3.1 with the same connection config.',
|
|
29
|
-
created: '2026-07-16T09:12:00Z',
|
|
30
|
-
referenced_interfaces: [{ interface_kind: 'connection', interface_name: 'sftp-partner' }],
|
|
31
|
-
};
|
|
32
|
-
const STAFF: ITicketThreadMessage = {
|
|
33
|
-
message_id: 'm2',
|
|
34
|
-
author_type: 'staff',
|
|
35
|
-
author_id: 'nick',
|
|
36
|
-
body: "Thanks for the detailed report — we've reproduced the timeout and a fix is in review. In the meantime, can you confirm the datasource pool size configured for the sftp-partner connection?",
|
|
37
|
-
created: '2026-07-16T10:03:00Z',
|
|
38
|
-
};
|
|
39
|
-
const INTERNAL: ITicketThreadMessage = {
|
|
40
|
-
message_id: 'm3',
|
|
41
|
-
author_type: 'staff',
|
|
42
|
-
author_id: 'nick',
|
|
43
|
-
internal: true,
|
|
44
|
-
body: 'Root cause: the connection-pool default silently dropped from 4 to 1 in the 3.2 migration. Patch is in review against develop.',
|
|
45
|
-
created: '2026-07-16T10:05:00Z',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const themed = (node: ReactNode) => (
|
|
49
|
-
<ReqoreUIProvider theme={{ main: '#121212', intents: { custom1: '#762f7e' } }}>
|
|
50
|
-
<div style={{ padding: 20, background: '#121212', maxWidth: 780 }}>{node}</div>
|
|
51
|
-
</ReqoreUIProvider>
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const refs = (
|
|
55
|
-
<InterfaceReferenceTags
|
|
56
|
-
size='tiny'
|
|
57
|
-
customTheme={{ main: 'custom1' }}
|
|
58
|
-
references={[{ interface_kind: 'connection', interface_name: 'sftp-partner' }]}
|
|
59
|
-
/>
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Staff view — the thread with `viewerRole='staff'` (internal notes show, amber)
|
|
64
|
-
* above the staff composer with the split send.
|
|
65
|
-
*/
|
|
66
|
-
export const StaffConversation: Story = {
|
|
67
|
-
parameters: {
|
|
68
|
-
docs: {
|
|
69
|
-
description: {
|
|
70
|
-
story:
|
|
71
|
-
"Renders the staff view — the thread with viewerRole='staff' (internal notes visible, amber) stacked above the staff composer with its split send.",
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
render: () =>
|
|
76
|
-
themed(
|
|
77
|
-
<>
|
|
78
|
-
<TicketThread viewerRole='staff' messages={[CUSTOMER, STAFF, INTERNAL]} />
|
|
79
|
-
<div style={{ marginTop: 16 }}>
|
|
80
|
-
<TicketReplyBox allowInternalNote onSend={fn()} aboveInput={refs} />
|
|
81
|
-
</div>
|
|
82
|
-
</>
|
|
83
|
-
),
|
|
84
|
-
async play({ canvasElement }) {
|
|
85
|
-
await expect(canvasElement.textContent).toContain('reproduced the timeout');
|
|
86
|
-
// the internal note is visible to staff
|
|
87
|
-
await expect(canvasElement.textContent).toContain('Root cause');
|
|
88
|
-
// the composer (rich editor) renders below the thread
|
|
89
|
-
await expect(canvasElement.querySelector('[contenteditable="true"]')).not.toBeNull();
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Customer view — the same thread as the customer sees it (`viewerRole='customer'`,
|
|
95
|
-
* staff shown as "Qorus Support", no internal notes) above their reply composer.
|
|
96
|
-
*/
|
|
97
|
-
export const CustomerConversation: Story = {
|
|
98
|
-
parameters: {
|
|
99
|
-
docs: {
|
|
100
|
-
description: {
|
|
101
|
-
story:
|
|
102
|
-
"Renders the customer view — the same thread with viewerRole='customer' (staff shown as 'Qorus Support', no internal notes) stacked above the customer's reply composer.",
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
render: () =>
|
|
107
|
-
themed(
|
|
108
|
-
<>
|
|
109
|
-
<TicketThread viewerRole='customer' messages={[CUSTOMER, STAFF]} />
|
|
110
|
-
<div style={{ marginTop: 16 }}>
|
|
111
|
-
<TicketReplyBox onSend={fn()} aboveInput={refs} />
|
|
112
|
-
</div>
|
|
113
|
-
</>
|
|
114
|
-
),
|
|
115
|
-
async play({ canvasElement }) {
|
|
116
|
-
await expect(canvasElement.textContent).toContain('reproduced the timeout');
|
|
117
|
-
// the internal note is not in the customer's messages
|
|
118
|
-
await expect(canvasElement.textContent).not.toContain('Root cause');
|
|
119
|
-
// the composer (rich editor) renders below the thread
|
|
120
|
-
await expect(canvasElement.querySelector('[contenteditable="true"]')).not.toBeNull();
|
|
121
|
-
},
|
|
122
|
-
};
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { ReqoreButton, ReqoreDropdown } from '@qoretechnologies/reqore';
|
|
2
|
-
import { StoryObj } from '@storybook/react-vite';
|
|
3
|
-
import { expect, within } from 'storybook/test';
|
|
4
|
-
import { StoryMeta } from '../../types';
|
|
5
|
-
import { TicketMetaTags } from './TicketMetaTags';
|
|
6
|
-
import {
|
|
7
|
-
TicketAvatar,
|
|
8
|
-
TicketHeader,
|
|
9
|
-
TicketHeaderText,
|
|
10
|
-
TicketPersonValue,
|
|
11
|
-
ticketInitials,
|
|
12
|
-
} from './TicketHeader';
|
|
13
|
-
|
|
14
|
-
const meta = {
|
|
15
|
-
component: TicketHeader,
|
|
16
|
-
title: 'Components/TicketHeader',
|
|
17
|
-
args: {
|
|
18
|
-
subject: 'Workflow deploy fails at the mapper step',
|
|
19
|
-
reference: '#1001',
|
|
20
|
-
},
|
|
21
|
-
} as StoryMeta<typeof TicketHeader>;
|
|
22
|
-
|
|
23
|
-
export default meta;
|
|
24
|
-
type Story = StoryObj<typeof meta>;
|
|
25
|
-
|
|
26
|
-
const person = (name: string) => (
|
|
27
|
-
<TicketPersonValue>
|
|
28
|
-
<TicketAvatar>{ticketInitials(name)}</TicketAvatar>
|
|
29
|
-
<TicketHeaderText>{name}</TicketHeaderText>
|
|
30
|
-
</TicketPersonValue>
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* What a customer sees: the same rail, every value read-only. The properties are the
|
|
35
|
-
* caller's — this is the shorter of the two lists a real surface passes.
|
|
36
|
-
*/
|
|
37
|
-
export const Customer: Story = {
|
|
38
|
-
parameters: {
|
|
39
|
-
docs: {
|
|
40
|
-
description: {
|
|
41
|
-
story:
|
|
42
|
-
"Renders the customer's header rail — subject, reference, and a short list of read-only property rows, with the status shown from the customer's viewpoint as 'Awaiting your reply'.",
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
args: {
|
|
47
|
-
properties: [
|
|
48
|
-
{
|
|
49
|
-
label: 'Status',
|
|
50
|
-
value: (
|
|
51
|
-
<TicketMetaTags
|
|
52
|
-
ticket={{ status: 'awaiting_customer', priority: 'high', category: 'bug' }}
|
|
53
|
-
viewerRole='customer'
|
|
54
|
-
/>
|
|
55
|
-
),
|
|
56
|
-
},
|
|
57
|
-
{ label: 'Opened by', value: person('ide-demo@local.dev') },
|
|
58
|
-
{ label: 'References', value: <TicketHeaderText muted>None</TicketHeaderText> },
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
async play({ canvasElement }) {
|
|
62
|
-
const canvas = within(canvasElement);
|
|
63
|
-
await expect(canvas.getByText('Workflow deploy fails at the mapper step')).toBeInTheDocument();
|
|
64
|
-
await expect(canvas.getByText('#1001')).toBeInTheDocument();
|
|
65
|
-
await expect(canvas.getByText('Awaiting your reply')).toBeInTheDocument();
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* What staff see: the same frame with pressable values. The row is as tall as a
|
|
71
|
-
* control whether or not it holds one, so a rail that mixes text and dropdowns still
|
|
72
|
-
* reads as a table — that invariant is what the alignment test below pins.
|
|
73
|
-
*/
|
|
74
|
-
export const Staff: Story = {
|
|
75
|
-
parameters: {
|
|
76
|
-
docs: {
|
|
77
|
-
description: {
|
|
78
|
-
story:
|
|
79
|
-
'Renders the staff header rail — the same frame with pressable values (a status dropdown, assignee and customer people, a references button) alongside plain text.',
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
args: {
|
|
84
|
-
properties: [
|
|
85
|
-
{
|
|
86
|
-
label: 'Status',
|
|
87
|
-
value: (
|
|
88
|
-
<ReqoreDropdown
|
|
89
|
-
size='small'
|
|
90
|
-
minimal
|
|
91
|
-
flat
|
|
92
|
-
label='Open'
|
|
93
|
-
showCaret={false}
|
|
94
|
-
rightIcon='ArrowDownSLine'
|
|
95
|
-
items={[{ label: 'Open', selected: true }, { label: 'Resolved' }]}
|
|
96
|
-
/>
|
|
97
|
-
),
|
|
98
|
-
},
|
|
99
|
-
{ label: 'Assignee', value: person('nick.m@acme.io') },
|
|
100
|
-
{ label: 'Customer', value: person('ide-demo@local.dev') },
|
|
101
|
-
{
|
|
102
|
-
label: 'References',
|
|
103
|
-
value: (
|
|
104
|
-
<ReqoreButton size='small' minimal flat icon='GitBranchLine'>
|
|
105
|
-
5 references
|
|
106
|
-
</ReqoreButton>
|
|
107
|
-
),
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
},
|
|
111
|
-
async play({ canvasElement }) {
|
|
112
|
-
const canvas = within(canvasElement);
|
|
113
|
-
await expect(canvas.getByText('nick.m@acme.io')).toBeInTheDocument();
|
|
114
|
-
await expect(canvas.getByText('NM')).toBeInTheDocument();
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The rail's two layout invariants, measured rather than eyeballed — both have
|
|
120
|
-
* regressed by hand more than once.
|
|
121
|
-
*
|
|
122
|
-
* 1. Every value starts on the same vertical edge, whether it is a control (which
|
|
123
|
-
* insets its own content by its horizontal padding) or bare text (which does not).
|
|
124
|
-
* 2. Every row is the same height, whether it holds a 32px control or a line of text.
|
|
125
|
-
*/
|
|
126
|
-
export const RowsAlign: Story = {
|
|
127
|
-
parameters: {
|
|
128
|
-
docs: {
|
|
129
|
-
description: {
|
|
130
|
-
story:
|
|
131
|
-
"Renders the staff rail again to measure its two layout invariants — every value's content starts on the same vertical edge, and every row is the same height, whether the row holds a control or bare text.",
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
args: Staff.args,
|
|
136
|
-
async play({ canvasElement }) {
|
|
137
|
-
const rows = Array.from(
|
|
138
|
-
canvasElement.querySelectorAll<HTMLElement>('.reqore-description-list-row')
|
|
139
|
-
);
|
|
140
|
-
await expect(rows.length).toBe(4);
|
|
141
|
-
|
|
142
|
-
/* Where the value's CONTENT starts, not where its box does. The inset is padding
|
|
143
|
-
and it lands on the value itself — the button on a control row, the person span
|
|
144
|
-
on a text row — so measure that child's padded edge. */
|
|
145
|
-
const contentLeft = (row: HTMLElement) => {
|
|
146
|
-
const column = row.querySelector<HTMLElement>('.reqore-description-list-content')!;
|
|
147
|
-
const value = (column.firstElementChild as HTMLElement | null) ?? column;
|
|
148
|
-
return Math.round(
|
|
149
|
-
value.getBoundingClientRect().left + parseFloat(getComputedStyle(value).paddingLeft)
|
|
150
|
-
);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const lefts = rows.map(contentLeft);
|
|
154
|
-
await expect(new Set(lefts).size, `value content starts at: ${lefts.join(', ')}`).toBe(1);
|
|
155
|
-
|
|
156
|
-
const heights = rows.map((row) => Math.round(row.getBoundingClientRect().height));
|
|
157
|
-
await expect(new Set(heights).size, `row heights: ${heights.join(', ')}`).toBe(1);
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
/** `when: false` drops a row, so one list can serve both viewer roles. */
|
|
162
|
-
export const HiddenRow: Story = {
|
|
163
|
-
parameters: {
|
|
164
|
-
docs: {
|
|
165
|
-
description: {
|
|
166
|
-
story:
|
|
167
|
-
'Renders a property list where one row is flagged when: false, showing that the flagged Assignee row drops out so a single list can serve both viewer roles.',
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
args: {
|
|
172
|
-
properties: [
|
|
173
|
-
{ label: 'Status', value: <TicketHeaderText>Open</TicketHeaderText> },
|
|
174
|
-
{ label: 'Assignee', value: <TicketHeaderText>nick.m@acme.io</TicketHeaderText>, when: false },
|
|
175
|
-
{ label: 'Customer', value: <TicketHeaderText>ide-demo@local.dev</TicketHeaderText> },
|
|
176
|
-
],
|
|
177
|
-
},
|
|
178
|
-
async play({ canvasElement }) {
|
|
179
|
-
const canvas = within(canvasElement);
|
|
180
|
-
await expect(canvas.queryByText('Assignee')).not.toBeInTheDocument();
|
|
181
|
-
await expect(canvas.getByText('Customer')).toBeInTheDocument();
|
|
182
|
-
},
|
|
183
|
-
};
|