@qoretechnologies/reqraft 0.10.65 → 0.10.66-pr.145.g2d1b4c8
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 +128 -24
- 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 +243 -85
- 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/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 +5 -3
- 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 +4 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.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/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.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/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +8 -16
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +141 -22
- 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 +358 -77
- 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/fields/ChoiceGroupHeading.tsx +38 -0
- package/src/components/form/fields/Field.tsx +6 -5
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +7 -4
- package/src/components/form/fields/radio-group/RadioGroup.tsx +13 -7
- package/src/components/form/index.tsx +1 -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/ticketThread/TicketThread.stories.tsx +31 -2
- package/src/components/ticketThread/TicketThread.tsx +13 -28
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// - `ConnectionManagement` per-item actions — dropped (actions omitted).
|
|
9
9
|
// - `Select` → reqraft `SelectFormField` (`items` instead of `defaultItems`,
|
|
10
10
|
// single-arg `onChange`; `type`/`closeCollectionOnUnMount` not forwarded).
|
|
11
|
-
import { ReqoreCheckbox, ReqoreControlGroup
|
|
11
|
+
import { ReqoreCheckbox, ReqoreControlGroup } from '@qoretechnologies/reqore';
|
|
12
12
|
import { IReqoreButtonProps } from '@qoretechnologies/reqore/dist/components/Button';
|
|
13
13
|
import { IQorusAllowedValue, TQorusType } from '@qoretechnologies/ts-toolkit';
|
|
14
14
|
import { size as count, isEqual } from 'lodash';
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from '../select/SelectCollection';
|
|
21
21
|
import { ISelectFormFieldItem, SelectFormField as Select } from '../select/Select';
|
|
22
22
|
import { useAllowedValueAvailability } from '../../engine/OptionFieldMessages';
|
|
23
|
+
import { ChoiceGroupHeading, useSelectedChoiceIntent } from '../ChoiceGroupHeading';
|
|
23
24
|
import type { IReqraftAllowedValue } from '../Field';
|
|
24
25
|
|
|
25
26
|
export interface IFieldAllowedValuesProps extends Pick<
|
|
@@ -97,13 +98,15 @@ export const FieldAllowedValuesCheckGroup = memo(
|
|
|
97
98
|
*/
|
|
98
99
|
label?: string;
|
|
99
100
|
}) => {
|
|
101
|
+
const selectedChoiceIntent = useSelectedChoiceIntent();
|
|
102
|
+
|
|
100
103
|
return (
|
|
101
104
|
<ReqoreControlGroup vertical size={rest.size || 'small'} gapSize='tiny'>
|
|
102
|
-
<
|
|
105
|
+
<ChoiceGroupHeading>
|
|
103
106
|
{label ? `${label}:`
|
|
104
107
|
: multiSelect ? 'Select one or more:'
|
|
105
108
|
: 'Select one:'}
|
|
106
|
-
</
|
|
109
|
+
</ChoiceGroupHeading>
|
|
107
110
|
{items?.map((item) => {
|
|
108
111
|
const unavailable = getSelectItemUnavailability(item);
|
|
109
112
|
|
|
@@ -134,7 +137,7 @@ export const FieldAllowedValuesCheckGroup = memo(
|
|
|
134
137
|
which keeps the pointer events the tooltip is read through.
|
|
135
138
|
It does not withhold `onClick` — the handler below does. */
|
|
136
139
|
readOnly={rest.readOnly || !!unavailable}
|
|
137
|
-
intent={item.value === value
|
|
140
|
+
intent={selectedChoiceIntent(item.value === value)}
|
|
138
141
|
// `isEqual`, and only once there is a value to match. The single
|
|
139
142
|
// -select branch compared JSON.stringify(value) with
|
|
140
143
|
// JSON.stringify(item.value), and JSON.stringify(undefined) is
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ReqoreRadioGroup } from '@qoretechnologies/reqore';
|
|
2
2
|
import { IReqoreRadioGroupProps } from '@qoretechnologies/reqore/dist/components/RadioGroup';
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
+
import { useIsCalmForm } from '../../engine/formAppearance';
|
|
4
5
|
|
|
5
6
|
export interface IRadioGroupFormFieldProps
|
|
6
7
|
extends Omit<IReqoreRadioGroupProps, 'onChange' | 'onSelectClick' | 'selected'> {
|
|
@@ -16,20 +17,25 @@ export const RadioGroupFormField = ({
|
|
|
16
17
|
value,
|
|
17
18
|
...rest
|
|
18
19
|
}: IRadioGroupFormFieldProps) => {
|
|
20
|
+
const calm = useIsCalmForm();
|
|
19
21
|
const items: IReqoreRadioGroupProps['items'] = useMemo(
|
|
20
22
|
() =>
|
|
21
23
|
_items.map((item) => ({
|
|
22
24
|
margin: 'right',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// Calm: an option's name as written — a choice is read, not shouted.
|
|
26
|
+
labelEffect:
|
|
27
|
+
calm ?
|
|
28
|
+
{ textSize: 'small' }
|
|
29
|
+
: {
|
|
30
|
+
spaced: 1,
|
|
31
|
+
weight: 'bold',
|
|
32
|
+
uppercase: true,
|
|
33
|
+
textSize: 'small',
|
|
34
|
+
},
|
|
29
35
|
...item,
|
|
30
36
|
disabled: disabled || item.disabled,
|
|
31
37
|
})),
|
|
32
|
-
[_items]
|
|
38
|
+
[_items, calm]
|
|
33
39
|
);
|
|
34
40
|
|
|
35
41
|
return (
|
|
@@ -24,6 +24,7 @@ export * from './fields/string/String';
|
|
|
24
24
|
export * from './fields/template/ExampleValueModal';
|
|
25
25
|
export * from './fields/template/TemplateField';
|
|
26
26
|
export * from './engine/FormEngine';
|
|
27
|
+
export * from './engine/formAppearance';
|
|
27
28
|
export * from './engine/FormFieldsSkeleton';
|
|
28
29
|
export * from './engine/optionActions';
|
|
29
30
|
export * from './engine/rendererTypes';
|
|
@@ -31,8 +31,9 @@ export interface IInterfaceReferenceTagsProps {
|
|
|
31
31
|
*/
|
|
32
32
|
onRemove?: (reference: IInterfaceReference) => void;
|
|
33
33
|
size?: TSizes;
|
|
34
|
-
/** Chip styling
|
|
35
|
-
*
|
|
34
|
+
/** Chip styling. Left out, a reference is a quiet tag with its kind's icon, as the
|
|
35
|
+
* support surfaces draw it; tint it only where the colour has a job (Qonsole's purple
|
|
36
|
+
* is Qonsole's alone — instruction-files decision 002). */
|
|
36
37
|
intent?: ComponentProps<typeof ReqoreTag>['intent'];
|
|
37
38
|
customTheme?: ComponentProps<typeof ReqoreTag>['customTheme'];
|
|
38
39
|
/** Resolve a per-reference image — e.g. a qog's trigger-app logo, the way the
|
|
@@ -29,8 +29,6 @@ const meta = {
|
|
|
29
29
|
export default meta;
|
|
30
30
|
type Story = StoryObj<typeof meta>;
|
|
31
31
|
|
|
32
|
-
const ACCENT = { main: 'custom1' } as const;
|
|
33
|
-
|
|
34
32
|
/* The kind ids a host passes — wire values, not labels. `qog` is the customer-facing
|
|
35
33
|
* kind for what the instance still lists as `fsm`; hosts map that at the fetch
|
|
36
34
|
* boundary, so `fsm` never reaches the picker. */
|
|
@@ -143,7 +141,6 @@ const Hosted = ({
|
|
|
143
141
|
their own remove — and read a size up from the thread's, where they're
|
|
144
142
|
a record rather than a control. */
|
|
145
143
|
<InterfaceReferenceTags
|
|
146
|
-
customTheme={ACCENT}
|
|
147
144
|
references={picked}
|
|
148
145
|
onRemove={(reference) =>
|
|
149
146
|
setPicked((current) =>
|
|
@@ -167,7 +164,6 @@ const Hosted = ({
|
|
|
167
164
|
flat
|
|
168
165
|
badge={picked.length || undefined}
|
|
169
166
|
active={open}
|
|
170
|
-
customTheme={ACCENT}
|
|
171
167
|
tooltip={open ? 'Hide references' : 'Reference interfaces'}
|
|
172
168
|
onClick={() => setOpen((shown) => !shown)}
|
|
173
169
|
/>
|
|
@@ -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}
|
|
@@ -159,6 +159,30 @@ const meta = {
|
|
|
159
159
|
export default meta;
|
|
160
160
|
type Story = StoryObj<typeof meta>;
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* How coloured a message's card is: the saturation (0–1) of its border, the one place
|
|
164
|
+
* ReqoreBubble draws its tint. A neutral card is near 0; amber or purple is well above.
|
|
165
|
+
*/
|
|
166
|
+
const cardSaturation = (messageId: string): number => {
|
|
167
|
+
const bubble = document.getElementById(`ticket-message-${messageId}`);
|
|
168
|
+
if (!bubble) throw new Error(`message ${messageId} not rendered`);
|
|
169
|
+
const bordered = [bubble, ...Array.from(bubble.querySelectorAll<HTMLElement>('*'))].find((el) => {
|
|
170
|
+
const style = getComputedStyle(el);
|
|
171
|
+
return parseFloat(style.borderTopWidth) > 0 && style.borderTopStyle !== 'none';
|
|
172
|
+
});
|
|
173
|
+
if (!bordered) throw new Error(`message ${messageId} has no card border`);
|
|
174
|
+
const [r, g, b] = getComputedStyle(bordered).borderTopColor.match(/[\d.]+/g)!.map(Number);
|
|
175
|
+
const max = Math.max(r, g, b);
|
|
176
|
+
return max === 0 ? 0 : (max - Math.min(r, g, b)) / max;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Neither side of a conversation is tinted (no purple, no blue); only an internal note is. */
|
|
180
|
+
const expectNeutralCards = async (ids: string[]) => {
|
|
181
|
+
for (const id of ids) {
|
|
182
|
+
await expect(cardSaturation(id)).toBeLessThan(0.2);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
162
186
|
/**
|
|
163
187
|
* The customer viewpoint: the customer's own messages sit on the right under
|
|
164
188
|
* "You", staff appear under a masked "Qorus Support" label, and internal notes
|
|
@@ -170,7 +194,7 @@ export const CustomerView: Story = {
|
|
|
170
194
|
docs: {
|
|
171
195
|
description: {
|
|
172
196
|
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.",
|
|
197
|
+
"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
198
|
},
|
|
175
199
|
},
|
|
176
200
|
},
|
|
@@ -186,6 +210,8 @@ export const CustomerView: Story = {
|
|
|
186
210
|
await expect(canvas.getByText('Qorus Support')).toBeInTheDocument();
|
|
187
211
|
// internal notes are filtered out for the customer
|
|
188
212
|
await expect(canvas.queryByText('Internal note')).not.toBeInTheDocument();
|
|
213
|
+
// the side, the avatar and the label say who spoke; no card is tinted for it
|
|
214
|
+
await expectNeutralCards(['m1', 'm2']);
|
|
189
215
|
|
|
190
216
|
await userEvent.click(canvas.getAllByText('deploy-trace.log')[0]);
|
|
191
217
|
await waitFor(() =>
|
|
@@ -306,7 +332,7 @@ export const StaffView: Story = {
|
|
|
306
332
|
docs: {
|
|
307
333
|
description: {
|
|
308
334
|
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.",
|
|
335
|
+
"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
336
|
},
|
|
311
337
|
},
|
|
312
338
|
},
|
|
@@ -320,6 +346,9 @@ export const StaffView: Story = {
|
|
|
320
346
|
await expect(canvas.getByText('Internal note')).toBeInTheDocument();
|
|
321
347
|
await expect(canvas.getByText(/Support · nick/)).toBeInTheDocument();
|
|
322
348
|
await expect(canvas.getByText(/Customer · acme/)).toBeInTheDocument();
|
|
349
|
+
// both sides are neutral cards; the internal note alone is amber
|
|
350
|
+
await expectNeutralCards(['m1', 'm2']);
|
|
351
|
+
await expect(cardSaturation('m3')).toBeGreaterThan(0.3);
|
|
323
352
|
// the reference chip renders with the built-in default icon (no resolver passed)
|
|
324
353
|
await expect(canvas.getByText('nightly-deploy:1.2')).toBeInTheDocument();
|
|
325
354
|
},
|
|
@@ -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';
|
|
@@ -32,12 +32,11 @@ import {
|
|
|
32
32
|
* the Qonsole `support-ticket` renderer. Presentational — data in, callbacks out
|
|
33
33
|
* — so each consumer keeps its own fetch/websocket wiring.
|
|
34
34
|
*
|
|
35
|
-
* Rendered as a comment feed: an avatar beside a bordered,
|
|
35
|
+
* Rendered as a comment feed: an avatar beside a bordered, neutral card per
|
|
36
36
|
* 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
|
|
37
|
+
* supplies the avatar, the bold author label and the alignment). Interface
|
|
38
|
+
* references render via the shared `InterfaceReferenceTags`, as quiet tags with
|
|
39
|
+
* the kind's icon; the status/priority/category vocabularies live in
|
|
41
40
|
* `../supportTicket/meta`.
|
|
42
41
|
*
|
|
43
42
|
* Runs of consecutive messages are wrapped in a `ReqoreBubbleGroup`, which prints
|
|
@@ -114,10 +113,11 @@ const AUTHOR_META: Record<TTicketMessageAuthor, IAuthorMeta> = {
|
|
|
114
113
|
system: { icon: 'InformationLine' },
|
|
115
114
|
};
|
|
116
115
|
|
|
117
|
-
//
|
|
118
|
-
// carry who is speaking, which
|
|
119
|
-
//
|
|
120
|
-
|
|
116
|
+
// No side is tinted: the side, the avatar and the author label ("You", "Qorus
|
|
117
|
+
// Support") already carry who is speaking, which leaves colour one job here —
|
|
118
|
+
// amber for an internal note. Never the product's purple, which a support thread
|
|
119
|
+
// does not wear (instruction-files decision 002: purple marks where you are, a
|
|
120
|
+
// chart's accent, and Qonsole).
|
|
121
121
|
|
|
122
122
|
/** Avatar glyph; an internal note carries the "not visible to the customer" eye. */
|
|
123
123
|
const authorIcon = (message: ITicketThreadMessage): IReqoreIconName =>
|
|
@@ -666,23 +666,9 @@ export const TicketThread = ({
|
|
|
666
666
|
avatar={{ icon: authorIcon(message) }}
|
|
667
667
|
label={authorLabel(message, viewerRole)}
|
|
668
668
|
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
|
-
}
|
|
669
|
+
// Both sides are neutral cards; an internal note is amber, so it can
|
|
670
|
+
// never be mistaken for a reply the customer sees.
|
|
671
|
+
intent={message.internal ? 'warning' : undefined}
|
|
686
672
|
flat={false}
|
|
687
673
|
minimal
|
|
688
674
|
>
|
|
@@ -698,7 +684,6 @@ export const TicketThread = ({
|
|
|
698
684
|
<div style={{ marginTop: 8 }}>
|
|
699
685
|
<InterfaceReferenceTags
|
|
700
686
|
size='tiny'
|
|
701
|
-
customTheme={ACCENT}
|
|
702
687
|
references={refsFor(message)}
|
|
703
688
|
resolveInterfaceIcon={resolveInterfaceIcon}
|
|
704
689
|
preferKindImage={preferKindImage}
|