@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g124fc92
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/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +130 -25
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +78 -15
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +48 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +260 -99
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts +18 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +22 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +27 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactRowContext.d.ts +5 -0
- package/dist/components/form/engine/compactRowContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowContext.js.map +1 -1
- package/dist/components/form/engine/compactRowStyles.d.ts +12 -0
- package/dist/components/form/engine/compactRowStyles.d.ts.map +1 -1
- package/dist/components/form/engine/compactRowStyles.js +72 -39
- package/dist/components/form/engine/compactRowStyles.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +3 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.js.map +1 -1
- package/dist/components/form/engine/formAppearance.d.ts +48 -0
- package/dist/components/form/engine/formAppearance.d.ts.map +1 -0
- package/dist/components/form/engine/formAppearance.js +50 -0
- package/dist/components/form/engine/formAppearance.js.map +1 -0
- package/dist/components/form/engine/readFirst.d.ts +14 -2
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +6 -2
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rowFocus.d.ts +9 -1
- package/dist/components/form/engine/rowFocus.d.ts.map +1 -1
- package/dist/components/form/engine/rowFocus.js +17 -5
- package/dist/components/form/engine/rowFocus.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +21 -0
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts +21 -0
- package/dist/components/form/fields/ChoiceGroupHeading.d.ts.map +1 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js +33 -0
- package/dist/components/form/fields/ChoiceGroupHeading.js.map +1 -0
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +13 -11
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +20 -7
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/allowed-values/savedValues.d.ts +30 -0
- package/dist/components/form/fields/allowed-values/savedValues.d.ts.map +1 -0
- package/dist/components/form/fields/allowed-values/savedValues.js +18 -0
- package/dist/components/form/fields/allowed-values/savedValues.js.map +1 -0
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +18 -8
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/long-string/LongString.d.ts +7 -1
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +9 -5
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.d.ts.map +1 -1
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js +6 -5
- package/dist/components/form/fields/multi-select/MultiSelectFormField.js.map +1 -1
- package/dist/components/form/fields/number/Number.d.ts +4 -1
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +13 -5
- package/dist/components/form/fields/number/Number.js.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.d.ts.map +1 -1
- package/dist/components/form/fields/radio-group/RadioGroup.js +13 -7
- package/dist/components/form/fields/radio-group/RadioGroup.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +71 -42
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +2 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/imageLightbox/ImageLightbox.d.ts.map +1 -1
- package/dist/components/imageLightbox/ImageLightbox.js +2 -0
- package/dist/components/imageLightbox/ImageLightbox.js.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +3 -2
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts +2 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.js +19 -13
- package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
- package/dist/components/supportTicket/TicketHeader.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketHeader.js +4 -1
- package/dist/components/supportTicket/TicketHeader.js.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.d.ts.map +1 -1
- package/dist/components/supportTicket/TicketReplyBox.js +29 -10
- package/dist/components/supportTicket/TicketReplyBox.js.map +1 -1
- package/dist/components/supportTicket/useLoadedImages.d.ts +10 -0
- package/dist/components/supportTicket/useLoadedImages.d.ts.map +1 -1
- package/dist/components/supportTicket/useLoadedImages.js +63 -1
- package/dist/components/supportTicket/useLoadedImages.js.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.js +6 -2
- package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +10 -16
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/dist/helpers/allowedValues.d.ts +12 -0
- package/dist/helpers/allowedValues.d.ts.map +1 -1
- package/dist/helpers/allowedValues.js +20 -1
- package/dist/helpers/allowedValues.js.map +1 -1
- package/dist/helpers/fieldDescription.d.ts +16 -0
- package/dist/helpers/fieldDescription.d.ts.map +1 -0
- package/dist/helpers/fieldDescription.js +40 -0
- package/dist/helpers/fieldDescription.js.map +1 -0
- package/dist/helpers/singleLineString.d.ts.map +1 -1
- package/dist/helpers/singleLineString.js +1 -0
- package/dist/helpers/singleLineString.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +143 -23
- package/src/components/form/engine/CompactToolbar.tsx +85 -16
- package/src/components/form/engine/FormEngine.stories.tsx +25 -13
- package/src/components/form/engine/FormEngine.tsx +367 -81
- package/src/components/form/engine/FormEngineAppearance.stories.tsx +540 -0
- package/src/components/form/engine/FormEngineStickyHeaders.stories.tsx +5 -1
- package/src/components/form/engine/OptionFieldMessages.tsx +73 -2
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +17 -4
- package/src/components/form/engine/compactRowContext.ts +5 -0
- package/src/components/form/engine/compactRowStyles.ts +218 -38
- package/src/components/form/engine/compactToolbarContext.ts +3 -0
- package/src/components/form/engine/formAppearance.ts +78 -0
- package/src/components/form/engine/readFirst.ts +16 -3
- package/src/components/form/engine/rowFocus.ts +17 -6
- package/src/components/form/expressions/renderExpressionToText.ts +15 -0
- package/src/components/form/fields/ChoiceGroupHeading.tsx +38 -0
- package/src/components/form/fields/Field.tsx +11 -9
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +21 -9
- package/src/components/form/fields/allowed-values/savedValues.ts +41 -0
- package/src/components/form/fields/auto/AutoFormField.tsx +17 -4
- package/src/components/form/fields/long-string/LongString.stories.tsx +27 -0
- package/src/components/form/fields/long-string/LongString.tsx +14 -3
- package/src/components/form/fields/multi-select/MultiSelectFormField.tsx +5 -4
- package/src/components/form/fields/number/Number.tsx +19 -4
- package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
- package/src/components/form/fields/template/TemplateField.tsx +32 -4
- package/src/components/form/index.tsx +2 -0
- package/src/components/imageLightbox/ImageLightbox.tsx +2 -0
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +3 -2
- package/src/components/supportTicket/ReferencePicker.stories.tsx +0 -4
- package/src/components/supportTicket/ReferencePicker.tsx +22 -12
- package/src/components/supportTicket/TicketConversation.stories.tsx +0 -1
- package/src/components/supportTicket/TicketHeader.tsx +2 -1
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +24 -21
- package/src/components/supportTicket/TicketReplyBox.tsx +27 -5
- package/src/components/supportTicket/useLoadedImages.ts +20 -0
- package/src/components/ticketReferences/TicketReferences.stories.tsx +26 -0
- package/src/components/ticketReferences/TicketReferences.tsx +12 -4
- package/src/components/ticketThread/TicketThread.stories.tsx +71 -2
- package/src/components/ticketThread/TicketThread.tsx +15 -28
- package/src/helpers/allowedValues.ts +19 -0
- package/src/helpers/fieldDescription.ts +39 -0
- package/src/helpers/singleLineString.ts +1 -0
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
ReqoreMenuItem,
|
|
5
5
|
ReqoreModal,
|
|
6
6
|
ReqoreSpinner,
|
|
7
|
+
useReqoreTheme,
|
|
7
8
|
} from '@qoretechnologies/reqore';
|
|
8
9
|
import { TReqoreBadge } from '@qoretechnologies/reqore/dist/components/Button';
|
|
10
|
+
import { getReadableColor } from '@qoretechnologies/reqore/dist/helpers/colors';
|
|
11
|
+
import { rgba } from 'polished';
|
|
9
12
|
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
|
10
13
|
import styled from 'styled-components';
|
|
11
14
|
import { thinScrollbar } from '../../helpers/scrollbar';
|
|
@@ -128,21 +131,22 @@ const Panes = styled.div`
|
|
|
128
131
|
* instead keeps a focus cue without the box. The selector is scoped under this wrapper,
|
|
129
132
|
* so it outranks Reqore's without `!important`.
|
|
130
133
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* a plain filter control
|
|
134
|
+
* The line is the theme's neutral hairline, a step brighter on focus. Only `$active` (the
|
|
135
|
+
* field has a query) colours it, in `$accent` — blue by default, the same signal
|
|
136
|
+
* `intent='info'` carries on a plain filter control: blue says a filter is narrowing the
|
|
137
|
+
* list, and nothing else (instruction-files decision 001). It is never the product's
|
|
138
|
+
* purple, which a support surface does not wear (decision 002).
|
|
134
139
|
*/
|
|
135
|
-
const Search = styled.div<{ $accent: string; $active?: boolean }>`
|
|
140
|
+
const Search = styled.div<{ $accent: string; $line: string; $focus: string; $active?: boolean }>`
|
|
136
141
|
padding: 6px 4px 4px;
|
|
137
|
-
border-bottom: 1px solid
|
|
138
|
-
${({ $accent, $active }) => ($active ? $accent : `${$accent}80`)};
|
|
142
|
+
border-bottom: 1px solid ${({ $accent, $line, $active }) => ($active ? $accent : $line)};
|
|
139
143
|
transition: border-color 0.15s ease-out;
|
|
140
144
|
|
|
141
145
|
.reqore-control-wrapper:focus-within {
|
|
142
146
|
outline: none;
|
|
143
147
|
}
|
|
144
148
|
&:focus-within {
|
|
145
|
-
border-bottom-color: ${({ $accent }) => $accent};
|
|
149
|
+
border-bottom-color: ${({ $accent, $focus, $active }) => ($active ? $accent : $focus)};
|
|
146
150
|
}
|
|
147
151
|
`;
|
|
148
152
|
|
|
@@ -152,8 +156,6 @@ const Note = styled.div`
|
|
|
152
156
|
font-size: 12px;
|
|
153
157
|
`;
|
|
154
158
|
|
|
155
|
-
const DEFAULT_ACCENT = '#762f7e';
|
|
156
|
-
|
|
157
159
|
/*
|
|
158
160
|
* How a picked row reads: `info`, tinted rather than filled.
|
|
159
161
|
*
|
|
@@ -241,7 +243,8 @@ export interface IReferencePickerProps {
|
|
|
241
243
|
* vocabulary.
|
|
242
244
|
*/
|
|
243
245
|
resolveKindLabel?: (kind: string) => string;
|
|
244
|
-
/** the underline
|
|
246
|
+
/** the search underline's colour while the list is filtered. Defaults to the theme's
|
|
247
|
+
* `info` blue — the colour that says a filter is narrowing a list. */
|
|
245
248
|
accent?: string;
|
|
246
249
|
/** shown above the panes — the composer's picked-reference chips, typically */
|
|
247
250
|
header?: ReactNode;
|
|
@@ -303,13 +306,15 @@ export const ReferencePicker = ({
|
|
|
303
306
|
disabled,
|
|
304
307
|
resolveInterfaceIcon = defaultInterfaceIcon,
|
|
305
308
|
resolveKindLabel = defaultInterfaceKindLabel,
|
|
306
|
-
accent
|
|
309
|
+
accent,
|
|
307
310
|
header,
|
|
308
311
|
onClose,
|
|
309
312
|
title = 'Reference interfaces',
|
|
310
313
|
layout = 'auto',
|
|
311
314
|
presentation = 'auto',
|
|
312
315
|
}: IReferencePickerProps) => {
|
|
316
|
+
const theme = useReqoreTheme();
|
|
317
|
+
const readable = getReadableColor(theme);
|
|
313
318
|
const [query, setQuery] = useState<string>('');
|
|
314
319
|
const needle = query.trim().toLowerCase();
|
|
315
320
|
|
|
@@ -510,7 +515,12 @@ export const ReferencePicker = ({
|
|
|
510
515
|
const body = (
|
|
511
516
|
<Frame ref={frameRef} $bare={asModal}>
|
|
512
517
|
{header}
|
|
513
|
-
<Search
|
|
518
|
+
<Search
|
|
519
|
+
$accent={accent ?? theme.intents?.info ?? readable}
|
|
520
|
+
$line={rgba(readable, 0.2)}
|
|
521
|
+
$focus={rgba(readable, 0.5)}
|
|
522
|
+
$active={!!needle}
|
|
523
|
+
>
|
|
514
524
|
<ReqoreInput
|
|
515
525
|
value={query}
|
|
516
526
|
onChange={(event) => setQuery((event.target as HTMLInputElement).value)}
|
|
@@ -201,7 +201,8 @@ export const TicketHeader = ({
|
|
|
201
201
|
and the strip would no longer line up with them. */
|
|
202
202
|
rowPadding='tiny'
|
|
203
203
|
labelWidth={LABEL_WIDTH}
|
|
204
|
-
uppercaseLabels
|
|
204
|
+
/* The labels' case is the theme's (`theme.uppercaseLabels`): an app that sets its
|
|
205
|
+
labels in sentence case gets "Opened by", not "OPENED BY". */
|
|
205
206
|
flat
|
|
206
207
|
transparent
|
|
207
208
|
padded={false}
|
|
@@ -16,6 +16,19 @@ const meta = {
|
|
|
16
16
|
export default meta;
|
|
17
17
|
type Story = StoryObj<typeof meta>;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The reply is sent with the white primary, never the Qonsole purple: a ticket reply is not a
|
|
21
|
+
* hand-off to Qonsole (instruction-files decision 002). Read from the drawn button, so a theme
|
|
22
|
+
* or a default that brings the purple back fails here.
|
|
23
|
+
*/
|
|
24
|
+
const expectWhiteSend = async (canvasElement: HTMLElement) => {
|
|
25
|
+
const send = within(canvasElement).getByText('Send reply').closest('button')!;
|
|
26
|
+
await waitFor(() => {
|
|
27
|
+
const [r, g, b] = getComputedStyle(send).backgroundColor.match(/\d+/g)!.map(Number);
|
|
28
|
+
expect(Math.min(r, g, b)).toBeGreaterThan(200);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
19
32
|
/**
|
|
20
33
|
* The customer composer, plain-editor variant (used here for a deterministic
|
|
21
34
|
* type→send test): an attach button (no extra menu → a plain button, not a
|
|
@@ -27,7 +40,7 @@ export const Default: Story = {
|
|
|
27
40
|
docs: {
|
|
28
41
|
description: {
|
|
29
42
|
story:
|
|
30
|
-
"Renders the customer composer's plain-editor variant — an attach button and Send, no internal-note toggle; typing a reply enables Send, which calls onSend with the body and internal: false.",
|
|
43
|
+
"Renders the customer composer's plain-editor variant — an attach button and Send, no internal-note toggle; typing a reply enables Send — the white primary, not the Qonsole purple — which calls onSend with the body and internal: false.",
|
|
31
44
|
},
|
|
32
45
|
},
|
|
33
46
|
},
|
|
@@ -39,6 +52,7 @@ export const Default: Story = {
|
|
|
39
52
|
await expect(canvas.queryByText('Internal note')).not.toBeInTheDocument();
|
|
40
53
|
|
|
41
54
|
await userEvent.type(textarea!, 'Here is my reply');
|
|
55
|
+
await expectWhiteSend(canvasElement);
|
|
42
56
|
await userEvent.click(canvas.getByText('Send reply'));
|
|
43
57
|
await waitFor(() =>
|
|
44
58
|
expect(args.onSend).toHaveBeenCalledWith(
|
|
@@ -59,7 +73,7 @@ export const StaffWithInternalNote: Story = {
|
|
|
59
73
|
docs: {
|
|
60
74
|
description: {
|
|
61
75
|
story:
|
|
62
|
-
"Renders the staff composer with a split send button — the primary action posts a public reply, while the caret opens 'Add internal note', which sends with internal: true.",
|
|
76
|
+
"Renders the staff composer with a split send button — the primary action (white, not the Qonsole purple) posts a public reply, while the caret opens 'Add internal note', which sends with internal: true.",
|
|
63
77
|
},
|
|
64
78
|
},
|
|
65
79
|
},
|
|
@@ -72,6 +86,7 @@ export const StaffWithInternalNote: Story = {
|
|
|
72
86
|
|
|
73
87
|
const textarea = canvasElement.querySelector('textarea');
|
|
74
88
|
await userEvent.type(textarea!, 'internal context');
|
|
89
|
+
await expectWhiteSend(canvasElement);
|
|
75
90
|
|
|
76
91
|
// "Add internal note" lives behind the split-send caret
|
|
77
92
|
const caret = canvasElement.querySelector('.ticket-send-more');
|
|
@@ -122,8 +137,7 @@ export const RichHelpdeskReply: Story = {
|
|
|
122
137
|
},
|
|
123
138
|
},
|
|
124
139
|
},
|
|
125
|
-
// Rendered under the Qorus theme
|
|
126
|
-
// `custom1` accent shows as the Qorus purple — a bare reqore theme greys it out.
|
|
140
|
+
// Rendered under the Qorus theme, as in qorus-ide / admin-portal.
|
|
127
141
|
decorators: [
|
|
128
142
|
(Story) => (
|
|
129
143
|
<ReqoreUIProvider theme={{ main: '#121212', intents: { custom1: '#762f7e' } }}>
|
|
@@ -141,23 +155,15 @@ export const RichHelpdeskReply: Story = {
|
|
|
141
155
|
{ label: 'Take screenshot', icon: 'CameraAiLine', onClick: fn() },
|
|
142
156
|
{ label: 'Capture area', icon: 'CropLine', onClick: fn() },
|
|
143
157
|
],
|
|
144
|
-
// the reference toggle
|
|
158
|
+
// the reference toggle: a quiet icon button, like the attach button beside it
|
|
145
159
|
footerActions: (
|
|
146
|
-
<ReqoreButton
|
|
147
|
-
icon='LinksLine'
|
|
148
|
-
minimal
|
|
149
|
-
flat
|
|
150
|
-
badge={1}
|
|
151
|
-
customTheme={{ main: 'custom1' }}
|
|
152
|
-
tooltip='Reference interfaces'
|
|
153
|
-
/>
|
|
160
|
+
<ReqoreButton icon='LinksLine' minimal flat badge={1} tooltip='Reference interfaces' />
|
|
154
161
|
),
|
|
155
|
-
// references on top of the editor (inside the bar),
|
|
156
|
-
// "References" label, the chips read as the referenced interfaces on their own
|
|
162
|
+
// references on top of the editor (inside the bar), as quiet tags with the kind's
|
|
163
|
+
// icon — no "References" label, the chips read as the referenced interfaces on their own
|
|
157
164
|
aboveInput: (
|
|
158
165
|
<InterfaceReferenceTags
|
|
159
166
|
references={[{ interface_kind: 'connection', interface_name: 'sftp-partner' }]}
|
|
160
|
-
customTheme={{ main: 'custom1' }}
|
|
161
167
|
/>
|
|
162
168
|
),
|
|
163
169
|
},
|
|
@@ -176,7 +182,7 @@ export const RichHelpdeskReply: Story = {
|
|
|
176
182
|
|
|
177
183
|
/**
|
|
178
184
|
* Every referenceable interface kind on one message, so the per-kind icons +
|
|
179
|
-
*
|
|
185
|
+
* chip styling can be reviewed together: workflow, service, job, fsm,
|
|
180
186
|
* connection, mapper, class, value-map, qog, ai-collection, ai-endpoint,
|
|
181
187
|
* ai-guardrail. (`qog` has no built-in icon → the generic fallback.)
|
|
182
188
|
*/
|
|
@@ -185,7 +191,7 @@ export const AllReferenceTypes: Story = {
|
|
|
185
191
|
docs: {
|
|
186
192
|
description: {
|
|
187
193
|
story:
|
|
188
|
-
'Renders a reply carrying every referenceable interface kind at once, so the per-kind icons and
|
|
194
|
+
'Renders a reply carrying every referenceable interface kind at once, so the per-kind icons and chip styling can be reviewed together (qog falls back to the generic glyph).',
|
|
189
195
|
},
|
|
190
196
|
},
|
|
191
197
|
},
|
|
@@ -202,7 +208,6 @@ export const AllReferenceTypes: Story = {
|
|
|
202
208
|
defaultText: 'This one references every interface kind.',
|
|
203
209
|
aboveInput: (
|
|
204
210
|
<InterfaceReferenceTags
|
|
205
|
-
customTheme={{ main: 'custom1' }}
|
|
206
211
|
references={[
|
|
207
212
|
{ interface_kind: 'workflow', interface_name: 'order-sync:1.2' },
|
|
208
213
|
{ interface_kind: 'service', interface_name: 'notifier' },
|
|
@@ -267,7 +272,6 @@ export const QogTriggerAppLogo: Story = {
|
|
|
267
272
|
defaultText: 'These qogs are referenced by their trigger apps.',
|
|
268
273
|
aboveInput: (
|
|
269
274
|
<InterfaceReferenceTags
|
|
270
|
-
customTheme={{ main: 'custom1' }}
|
|
271
275
|
references={[
|
|
272
276
|
// resolved live from trigger_app (the customer/IDE path)
|
|
273
277
|
{ interface_kind: 'qog', interface_name: 'telegram-intake', trigger_app: 'Telegram' },
|
|
@@ -337,7 +341,6 @@ export const WithScreenshotAndReferences: Story = {
|
|
|
337
341
|
defaultFiles: [screenshotFile()],
|
|
338
342
|
aboveInput: (
|
|
339
343
|
<InterfaceReferenceTags
|
|
340
|
-
customTheme={{ main: 'custom1' }}
|
|
341
344
|
references={[
|
|
342
345
|
{ interface_kind: 'connection', interface_name: 'sftp-partner' },
|
|
343
346
|
{ interface_kind: 'qog', interface_name: 'telegram-intake', logo: appLogo('#2AABEE', 'T') },
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import type { TReqoreDropdownItems } from '@qoretechnologies/reqore/dist/components/Dropdown/list';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Composer,
|
|
5
|
+
IComposerProps,
|
|
6
|
+
IComposerSendAction,
|
|
7
|
+
TComposerEditor,
|
|
8
|
+
} from '../composer/Composer';
|
|
4
9
|
import { fileToAttachment as defaultFileToAttachment } from './fileToAttachment';
|
|
5
10
|
import { IAttachmentUpload } from './meta';
|
|
6
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A ticket reply is not a hand-off to Qonsole, so it does not wear Qonsole's look: no purple
|
|
14
|
+
* bar, no purple send (instruction-files decision 002 — a purple button means "this hands the
|
|
15
|
+
* work to Qonsole / AI" and nothing else). The bar keeps the composer's shape with the theme's
|
|
16
|
+
* own neutral edge (its `main`, a step lighter — how Reqore draws its dividers), and sending
|
|
17
|
+
* the reply is the surface's one primary action: white.
|
|
18
|
+
*/
|
|
19
|
+
const REPLY_BOX_EFFECT: IComposerProps['effect'] = {
|
|
20
|
+
gradient: { direction: 'to bottom right', colors: 'main:lighten:3' },
|
|
21
|
+
};
|
|
22
|
+
const REPLY_SEND_STYLE: Pick<IComposerSendAction, 'intent' | 'minimal'> = {
|
|
23
|
+
intent: 'white',
|
|
24
|
+
minimal: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
7
27
|
/**
|
|
8
28
|
* The `items` shape ReqoreDropdown accepts. Named, not derived: `ReqoreDropdown`
|
|
9
29
|
* is a generic component, and `ComponentProps` of a generic resolves to `{}`
|
|
@@ -96,16 +116,15 @@ export const TicketReplyBox = ({
|
|
|
96
116
|
: 'Send reply';
|
|
97
117
|
|
|
98
118
|
// Staff surface splits the send button; customer surface is a single "Send reply".
|
|
99
|
-
// Both
|
|
100
|
-
//
|
|
119
|
+
// Both send the reply as the white primary (`REPLY_SEND_STYLE`); the internal note
|
|
120
|
+
// stays amber (`warning`) so it can't be mistaken for a public reply.
|
|
101
121
|
const sendActions: IComposerSendAction[] | undefined = allowInternalNote
|
|
102
122
|
? [
|
|
103
123
|
{
|
|
104
124
|
id: 'reply',
|
|
105
125
|
label: labelFor(false),
|
|
106
126
|
icon: 'SendPlane2Line',
|
|
107
|
-
|
|
108
|
-
customTheme: { main: 'custom1' },
|
|
127
|
+
...REPLY_SEND_STYLE,
|
|
109
128
|
},
|
|
110
129
|
{
|
|
111
130
|
id: 'internal',
|
|
@@ -139,7 +158,10 @@ export const TicketReplyBox = ({
|
|
|
139
158
|
disabledReason={disabledReason}
|
|
140
159
|
onSend={handleSend}
|
|
141
160
|
sendLabel={labelFor(false)}
|
|
161
|
+
sendIntent={REPLY_SEND_STYLE.intent}
|
|
162
|
+
sendMinimal={REPLY_SEND_STYLE.minimal}
|
|
142
163
|
sendActions={sendActions}
|
|
164
|
+
effect={REPLY_BOX_EFFECT}
|
|
143
165
|
sendMenuClassName='ticket-send-more'
|
|
144
166
|
attachMenuItems={attachMenuItems}
|
|
145
167
|
footerActions={footerActions}
|
|
@@ -66,6 +66,26 @@ export const useLoadedImages = (sources: Array<string | undefined>): ((src?: str
|
|
|
66
66
|
export const whenImageSettled = (src: string): Promise<void> =>
|
|
67
67
|
imageStatus.has(src) ? Promise.resolve() : loadImage(src);
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Resolves with `src` once it has loaded as an image, and rejects when it does not.
|
|
71
|
+
*
|
|
72
|
+
* A fetched attachment URL can resolve and still not be a picture — the server
|
|
73
|
+
* answered with an error page or an empty body — and an `<img>` handed it draws a
|
|
74
|
+
* broken picture. Chained after the fetch, this puts that case on the same path as
|
|
75
|
+
* a failed fetch, so every preview falls back to the file's name or its download
|
|
76
|
+
* chip. A rejected object URL is revoked here, since the caller never receives it.
|
|
77
|
+
*/
|
|
78
|
+
export const loadedImageUrl = async (src: string): Promise<string> => {
|
|
79
|
+
await whenImageSettled(src);
|
|
80
|
+
if (imageStatus.get(src) !== 'loaded') {
|
|
81
|
+
if (src.startsWith('blob:')) {
|
|
82
|
+
URL.revokeObjectURL(src);
|
|
83
|
+
}
|
|
84
|
+
throw new Error('The attachment is not an image that can be shown');
|
|
85
|
+
}
|
|
86
|
+
return src;
|
|
87
|
+
};
|
|
88
|
+
|
|
69
89
|
/** Forget every image's status. For tests only. */
|
|
70
90
|
export const resetLoadedImagesForTests = (): void => {
|
|
71
91
|
imageStatus.clear();
|
|
@@ -2,6 +2,7 @@ import { StoryObj } from '@storybook/react-vite';
|
|
|
2
2
|
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
3
3
|
|
|
4
4
|
import { StoryMeta } from '../../types';
|
|
5
|
+
import { STORY_BROKEN_IMAGE } from '../supportTicket/__fixtures__/kindMarks';
|
|
5
6
|
import { IInterfaceReference } from '../supportTicket/meta';
|
|
6
7
|
import { ITicketReferenceAttachment, TicketReferences } from './TicketReferences';
|
|
7
8
|
|
|
@@ -215,6 +216,31 @@ export const NoPreview: Story = {
|
|
|
215
216
|
},
|
|
216
217
|
};
|
|
217
218
|
|
|
219
|
+
/** A fetch that answers with bytes that are not a picture reads like a failed fetch. */
|
|
220
|
+
export const PreviewThatDoesNotLoad: Story = {
|
|
221
|
+
parameters: {
|
|
222
|
+
docs: {
|
|
223
|
+
description: {
|
|
224
|
+
story:
|
|
225
|
+
'Renders the references view where one screenshot\'s bytes are not an image the browser can draw: that tile shows its file name, like a screenshot with no preview, while the others keep their thumbnails.',
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
args: {
|
|
230
|
+
fetchAttachmentUrl: (id: string) =>
|
|
231
|
+
Promise.resolve(id === 'img-1' ? STORY_BROKEN_IMAGE : IMG_URLS[id] ?? svgUrl(id, '#555555')),
|
|
232
|
+
},
|
|
233
|
+
play: async ({ canvasElement }) => {
|
|
234
|
+
const tile = (name: string) =>
|
|
235
|
+
canvasElement.querySelector<HTMLElement>(`button[title="${name}"]`);
|
|
236
|
+
// the screenshots that load are thumbnails …
|
|
237
|
+
await waitFor(() => expect(tile('pool-metrics.png')?.querySelector('img')).not.toBeNull());
|
|
238
|
+
// … and the one that does not is its file name, never a broken picture
|
|
239
|
+
await waitFor(() => expect(tile('error-screenshot.png')?.textContent).not.toContain('…'));
|
|
240
|
+
await expect(tile('error-screenshot.png')?.querySelector('img')).toBeNull();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
218
244
|
/** Nothing attached or linked. */
|
|
219
245
|
export const Empty: Story = {
|
|
220
246
|
parameters: {
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
TResolveInterfaceIcon,
|
|
28
28
|
TTicketViewerRole,
|
|
29
29
|
} from '../supportTicket/meta';
|
|
30
|
-
import { useLoadedImages } from '../supportTicket/useLoadedImages';
|
|
30
|
+
import { loadedImageUrl, useLoadedImages } from '../supportTicket/useLoadedImages';
|
|
31
31
|
import type { ITicketThreadAttachment, ITicketThreadMessage } from '../ticketThread/TicketThread';
|
|
32
32
|
|
|
33
33
|
/*
|
|
@@ -241,7 +241,13 @@ const useImageUrls = (
|
|
|
241
241
|
}
|
|
242
242
|
let active = true;
|
|
243
243
|
const created: string[] = [];
|
|
244
|
-
Promise.allSettled(
|
|
244
|
+
Promise.allSettled(
|
|
245
|
+
ids.map((id) =>
|
|
246
|
+
fetchAttachmentUrl(id)
|
|
247
|
+
.then(loadedImageUrl)
|
|
248
|
+
.then((url) => ({ id, url }))
|
|
249
|
+
)
|
|
250
|
+
).then(
|
|
245
251
|
(results) => {
|
|
246
252
|
if (!active) {
|
|
247
253
|
return;
|
|
@@ -324,11 +330,13 @@ const Thumb = styled.button`
|
|
|
324
330
|
justify-content: center;
|
|
325
331
|
position: relative;
|
|
326
332
|
transition: border-color 0.15s;
|
|
333
|
+
/* A screenshot is something to read, not a hand-off to Qonsole, so its hover and focus
|
|
334
|
+
brighten the tile's own edge instead of wearing the product's purple (decision 002). */
|
|
327
335
|
&:hover {
|
|
328
|
-
border-color: rgba(
|
|
336
|
+
border-color: rgba(255, 255, 255, 0.35);
|
|
329
337
|
}
|
|
330
338
|
&:focus-visible {
|
|
331
|
-
outline: 2px solid
|
|
339
|
+
outline: 2px solid rgba(255, 255, 255, 0.6);
|
|
332
340
|
outline-offset: 2px;
|
|
333
341
|
}
|
|
334
342
|
img {
|
|
@@ -4,6 +4,7 @@ import styled from 'styled-components';
|
|
|
4
4
|
|
|
5
5
|
import { thinScrollbar } from '../../helpers/scrollbar';
|
|
6
6
|
import { StoryMeta } from '../../types';
|
|
7
|
+
import { STORY_BROKEN_IMAGE } from '../supportTicket/__fixtures__/kindMarks';
|
|
7
8
|
import type { IInterfaceReference, TTicketMessageAuthor } from '../supportTicket/meta';
|
|
8
9
|
import { ITicketThreadMessage, TicketThread } from './TicketThread';
|
|
9
10
|
|
|
@@ -159,6 +160,30 @@ const meta = {
|
|
|
159
160
|
export default meta;
|
|
160
161
|
type Story = StoryObj<typeof meta>;
|
|
161
162
|
|
|
163
|
+
/**
|
|
164
|
+
* How coloured a message's card is: the saturation (0–1) of its border, the one place
|
|
165
|
+
* ReqoreBubble draws its tint. A neutral card is near 0; amber or purple is well above.
|
|
166
|
+
*/
|
|
167
|
+
const cardSaturation = (messageId: string): number => {
|
|
168
|
+
const bubble = document.getElementById(`ticket-message-${messageId}`);
|
|
169
|
+
if (!bubble) throw new Error(`message ${messageId} not rendered`);
|
|
170
|
+
const bordered = [bubble, ...Array.from(bubble.querySelectorAll<HTMLElement>('*'))].find((el) => {
|
|
171
|
+
const style = getComputedStyle(el);
|
|
172
|
+
return parseFloat(style.borderTopWidth) > 0 && style.borderTopStyle !== 'none';
|
|
173
|
+
});
|
|
174
|
+
if (!bordered) throw new Error(`message ${messageId} has no card border`);
|
|
175
|
+
const [r, g, b] = getComputedStyle(bordered).borderTopColor.match(/[\d.]+/g)!.map(Number);
|
|
176
|
+
const max = Math.max(r, g, b);
|
|
177
|
+
return max === 0 ? 0 : (max - Math.min(r, g, b)) / max;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Neither side of a conversation is tinted (no purple, no blue); only an internal note is. */
|
|
181
|
+
const expectNeutralCards = async (ids: string[]) => {
|
|
182
|
+
for (const id of ids) {
|
|
183
|
+
await expect(cardSaturation(id)).toBeLessThan(0.2);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
162
187
|
/**
|
|
163
188
|
* The customer viewpoint: the customer's own messages sit on the right under
|
|
164
189
|
* "You", staff appear under a masked "Qorus Support" label, and internal notes
|
|
@@ -170,7 +195,7 @@ export const CustomerView: Story = {
|
|
|
170
195
|
docs: {
|
|
171
196
|
description: {
|
|
172
197
|
story:
|
|
173
|
-
"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.",
|
|
198
|
+
"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, both as neutral cards (the side and the label say who spoke), internal notes are filtered out, and clicking an attachment chip calls onDownloadAttachment with its id and filename.",
|
|
174
199
|
},
|
|
175
200
|
},
|
|
176
201
|
},
|
|
@@ -186,6 +211,8 @@ export const CustomerView: Story = {
|
|
|
186
211
|
await expect(canvas.getByText('Qorus Support')).toBeInTheDocument();
|
|
187
212
|
// internal notes are filtered out for the customer
|
|
188
213
|
await expect(canvas.queryByText('Internal note')).not.toBeInTheDocument();
|
|
214
|
+
// the side, the avatar and the label say who spoke; no card is tinted for it
|
|
215
|
+
await expectNeutralCards(['m1', 'm2']);
|
|
189
216
|
|
|
190
217
|
await userEvent.click(canvas.getAllByText('deploy-trace.log')[0]);
|
|
191
218
|
await waitFor(() =>
|
|
@@ -306,7 +333,7 @@ export const StaffView: Story = {
|
|
|
306
333
|
docs: {
|
|
307
334
|
description: {
|
|
308
335
|
story:
|
|
309
|
-
"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.",
|
|
336
|
+
"Renders the thread from the staff viewpoint: staff messages sit on the right, the customer on the left, both as neutral cards, and staff-only internal notes render amber and bordered under an 'Internal note' label.",
|
|
310
337
|
},
|
|
311
338
|
},
|
|
312
339
|
},
|
|
@@ -320,6 +347,9 @@ export const StaffView: Story = {
|
|
|
320
347
|
await expect(canvas.getByText('Internal note')).toBeInTheDocument();
|
|
321
348
|
await expect(canvas.getByText(/Support · nick/)).toBeInTheDocument();
|
|
322
349
|
await expect(canvas.getByText(/Customer · acme/)).toBeInTheDocument();
|
|
350
|
+
// both sides are neutral cards; the internal note alone is amber
|
|
351
|
+
await expectNeutralCards(['m1', 'm2']);
|
|
352
|
+
await expect(cardSaturation('m3')).toBeGreaterThan(0.3);
|
|
323
353
|
// the reference chip renders with the built-in default icon (no resolver passed)
|
|
324
354
|
await expect(canvas.getByText('nightly-deploy:1.2')).toBeInTheDocument();
|
|
325
355
|
},
|
|
@@ -749,6 +779,45 @@ export const ImagePreview: Story = {
|
|
|
749
779
|
},
|
|
750
780
|
};
|
|
751
781
|
|
|
782
|
+
/**
|
|
783
|
+
* An image attachment whose bytes are not a picture — the server answered with an error
|
|
784
|
+
* page or an empty body — is a download chip like any other file, never a broken image.
|
|
785
|
+
*/
|
|
786
|
+
export const ImageThatDoesNotLoad: Story = {
|
|
787
|
+
parameters: {
|
|
788
|
+
docs: {
|
|
789
|
+
description: {
|
|
790
|
+
story:
|
|
791
|
+
'Renders a message with two screenshots where one\'s bytes cannot be drawn: that one falls back to its download chip while the other stays a thumbnail.',
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
args: {
|
|
796
|
+
viewerRole: 'customer',
|
|
797
|
+
fetchAttachmentUrl: async (id: string) =>
|
|
798
|
+
id === 'img-1' ? STORY_BROKEN_IMAGE : screenshot('after', 205),
|
|
799
|
+
messages: [
|
|
800
|
+
{
|
|
801
|
+
message_id: 'img',
|
|
802
|
+
author_type: 'customer',
|
|
803
|
+
author_id: 'acme',
|
|
804
|
+
body: 'Here is the canvas before and after the upgrade:',
|
|
805
|
+
created: ago(6),
|
|
806
|
+
attachments: [
|
|
807
|
+
{ attachment_id: 'img-1', filename: 'before.png', content_type: 'image/png', size_bytes: 512_000 },
|
|
808
|
+
{ attachment_id: 'img-2', filename: 'after.png', content_type: 'image/png', size_bytes: 486_000 },
|
|
809
|
+
],
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
},
|
|
813
|
+
async play({ canvasElement }) {
|
|
814
|
+
const canvas = within(canvasElement);
|
|
815
|
+
await waitFor(() => expect(canvasElement.querySelector('img[alt="after.png"]')).not.toBeNull());
|
|
816
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: /before\.png/ })).toBeInTheDocument());
|
|
817
|
+
await expect(canvasElement.querySelector('img[alt="before.png"]')).toBeNull();
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
|
|
752
821
|
/**
|
|
753
822
|
* One message carrying the whole stack at once — a formatted body, image thumbnails,
|
|
754
823
|
* file chips, and interface references — so the vertical order and spacing of all four
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { getReadableColor } from '@qoretechnologies/reqore/dist/helpers/colors';
|
|
14
14
|
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
15
15
|
import { rgba } from 'polished';
|
|
16
|
-
import {
|
|
16
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
17
17
|
import ReactMarkdown from 'react-markdown';
|
|
18
18
|
import styled from 'styled-components';
|
|
19
19
|
import { formatBytes, isImage } from '../../helpers/common';
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
TTicketMessageAuthor,
|
|
26
26
|
TTicketViewerRole,
|
|
27
27
|
} from '../supportTicket';
|
|
28
|
+
import { loadedImageUrl } from '../supportTicket/useLoadedImages';
|
|
28
29
|
|
|
29
30
|
/*
|
|
30
31
|
* Shared support-ticket conversation thread, used by every Qorus surface that
|
|
@@ -32,12 +33,11 @@ import {
|
|
|
32
33
|
* the Qonsole `support-ticket` renderer. Presentational — data in, callbacks out
|
|
33
34
|
* — so each consumer keeps its own fetch/websocket wiring.
|
|
34
35
|
*
|
|
35
|
-
* Rendered as a comment feed: an avatar beside a bordered,
|
|
36
|
+
* Rendered as a comment feed: an avatar beside a bordered, neutral card per
|
|
36
37
|
* message, each hugging its own side. Built on Reqore's `ReqoreBubble` (which
|
|
37
|
-
* supplies the avatar, the bold author label
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* `InterfaceReferenceTags`; the status/priority/category vocabularies live in
|
|
38
|
+
* supplies the avatar, the bold author label and the alignment). Interface
|
|
39
|
+
* references render via the shared `InterfaceReferenceTags`, as quiet tags with
|
|
40
|
+
* the kind's icon; the status/priority/category vocabularies live in
|
|
41
41
|
* `../supportTicket/meta`.
|
|
42
42
|
*
|
|
43
43
|
* Runs of consecutive messages are wrapped in a `ReqoreBubbleGroup`, which prints
|
|
@@ -114,10 +114,11 @@ const AUTHOR_META: Record<TTicketMessageAuthor, IAuthorMeta> = {
|
|
|
114
114
|
system: { icon: 'InformationLine' },
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
// carry who is speaking, which
|
|
119
|
-
//
|
|
120
|
-
|
|
117
|
+
// No side is tinted: the side, the avatar and the author label ("You", "Qorus
|
|
118
|
+
// Support") already carry who is speaking, which leaves colour one job here —
|
|
119
|
+
// amber for an internal note. Never the product's purple, which a support thread
|
|
120
|
+
// does not wear (instruction-files decision 002: purple marks where you are, a
|
|
121
|
+
// chart's accent, and Qonsole).
|
|
121
122
|
|
|
122
123
|
/** Avatar glyph; an internal note carries the "not visible to the customer" eye. */
|
|
123
124
|
const authorIcon = (message: ITicketThreadMessage): IReqoreIconName =>
|
|
@@ -444,6 +445,7 @@ const ImageThumb = ({
|
|
|
444
445
|
useEffect(() => {
|
|
445
446
|
let alive = true;
|
|
446
447
|
fetchAttachmentUrl(attachment.attachment_id)
|
|
448
|
+
.then(loadedImageUrl)
|
|
447
449
|
.then((resolved) => alive && setUrl(resolved))
|
|
448
450
|
.catch(() => alive && setFailed(true));
|
|
449
451
|
return () => {
|
|
@@ -666,23 +668,9 @@ export const TicketThread = ({
|
|
|
666
668
|
avatar={{ icon: authorIcon(message) }}
|
|
667
669
|
label={authorLabel(message, viewerRole)}
|
|
668
670
|
timestamp={message.created ? <ReqoreTimeAgo time={message.created} /> : undefined}
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
|
|
672
|
-
// answerable at a glance. Deliberately keyed off `viewerRole`, so it
|
|
673
|
-
// stays correct on the staff surface too (staff replies are the blue
|
|
674
|
-
// ones there). Internal notes keep their warning intent and are not
|
|
675
|
-
// re-tinted — they are neither side of the conversation.
|
|
676
|
-
intent={
|
|
677
|
-
message.internal
|
|
678
|
-
? 'warning'
|
|
679
|
-
: message.author_type === viewerRole
|
|
680
|
-
? 'info'
|
|
681
|
-
: undefined
|
|
682
|
-
}
|
|
683
|
-
customTheme={
|
|
684
|
-
message.internal || message.author_type === viewerRole ? undefined : ACCENT
|
|
685
|
-
}
|
|
671
|
+
// Both sides are neutral cards; an internal note is amber, so it can
|
|
672
|
+
// never be mistaken for a reply the customer sees.
|
|
673
|
+
intent={message.internal ? 'warning' : undefined}
|
|
686
674
|
flat={false}
|
|
687
675
|
minimal
|
|
688
676
|
>
|
|
@@ -698,7 +686,6 @@ export const TicketThread = ({
|
|
|
698
686
|
<div style={{ marginTop: 8 }}>
|
|
699
687
|
<InterfaceReferenceTags
|
|
700
688
|
size='tiny'
|
|
701
|
-
customTheme={ACCENT}
|
|
702
689
|
references={refsFor(message)}
|
|
703
690
|
resolveInterfaceIcon={resolveInterfaceIcon}
|
|
704
691
|
preferKindImage={preferKindImage}
|
|
@@ -79,6 +79,25 @@ export const findAllowedValueOption = (
|
|
|
79
79
|
);
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The value a choice stands for, whichever of the three ways its entry is
|
|
84
|
+
* written (see `findAllowedValueOption`): the envelope's `value`, a bare
|
|
85
|
+
* `value`, or the entry's `name`.
|
|
86
|
+
*
|
|
87
|
+
* The row that names a stored choice and the validator that accepts it have
|
|
88
|
+
* always read all three. The pickers that WRITE a choice read only the
|
|
89
|
+
* envelope, so a list written bare showed nothing selected for the stored value
|
|
90
|
+
* and stored `undefined` when a choice was clicked: picking a pipeline
|
|
91
|
+
* processor's class cleared the field instead of setting it.
|
|
92
|
+
*/
|
|
93
|
+
export const allowedValueOf = (option: any): unknown => {
|
|
94
|
+
const value = option?.value;
|
|
95
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && 'value' in value) {
|
|
96
|
+
return value.value;
|
|
97
|
+
}
|
|
98
|
+
return value !== undefined ? value : option?.name;
|
|
99
|
+
};
|
|
100
|
+
|
|
82
101
|
/**
|
|
83
102
|
* A Slate rich-text document: a non-empty array of element nodes, each with
|
|
84
103
|
* `children`. Told apart from a LIST value, which is an array of elements.
|