@qoretechnologies/reqraft 0.10.57 → 0.10.58-pr.129.g43f4175
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/design/FORM_ENGINE_COMPACT.md +23 -5
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +2 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +35 -8
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +6 -3
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.d.ts.map +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.js +1 -1
- package/dist/components/form/engine/variants/VariantCalmTable.js.map +1 -1
- package/dist/components/form/engine/variants/variantModel.d.ts.map +1 -1
- package/dist/components/form/engine/variants/variantModel.js +8 -3
- package/dist/components/form/engine/variants/variantModel.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAuto.js +8 -0
- package/dist/components/form/fields/array/ArrayAuto.js.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.d.ts.map +1 -1
- package/dist/components/form/fields/array/ArrayAutoField.js +8 -0
- package/dist/components/form/fields/array/ArrayAutoField.js.map +1 -1
- package/dist/components/form/fields/binary/Binary.d.ts.map +1 -1
- package/dist/components/form/fields/binary/Binary.js +10 -0
- package/dist/components/form/fields/binary/Binary.js.map +1 -1
- package/dist/components/form/fields/emittedValues.d.ts +38 -0
- package/dist/components/form/fields/emittedValues.d.ts.map +1 -0
- package/dist/components/form/fields/emittedValues.js +67 -0
- package/dist/components/form/fields/emittedValues.js.map +1 -0
- package/dist/components/form/fields/long-string/LongString.d.ts.map +1 -1
- package/dist/components/form/fields/long-string/LongString.js +24 -8
- package/dist/components/form/fields/long-string/LongString.js.map +1 -1
- package/dist/components/form/fields/number/Number.d.ts.map +1 -1
- package/dist/components/form/fields/number/Number.js +9 -0
- package/dist/components/form/fields/number/Number.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +10 -0
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts +21 -5
- package/dist/components/supportTicket/InterfaceReferenceTags.d.ts.map +1 -1
- package/dist/components/supportTicket/InterfaceReferenceTags.js +24 -8
- package/dist/components/supportTicket/InterfaceReferenceTags.js.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.d.ts +5 -4
- package/dist/components/supportTicket/ReferencePicker.d.ts.map +1 -1
- package/dist/components/supportTicket/ReferencePicker.js +27 -2
- package/dist/components/supportTicket/ReferencePicker.js.map +1 -1
- package/dist/components/supportTicket/meta.d.ts +16 -0
- package/dist/components/supportTicket/meta.d.ts.map +1 -1
- package/dist/components/supportTicket/meta.js +6 -1
- package/dist/components/supportTicket/meta.js.map +1 -1
- package/dist/components/supportTicket/useLoadedImages.d.ts +18 -0
- package/dist/components/supportTicket/useLoadedImages.d.ts.map +1 -0
- package/dist/components/supportTicket/useLoadedImages.js +72 -0
- package/dist/components/supportTicket/useLoadedImages.js.map +1 -0
- package/dist/components/ticketReferences/TicketReferences.d.ts +9 -5
- package/dist/components/ticketReferences/TicketReferences.d.ts.map +1 -1
- package/dist/components/ticketReferences/TicketReferences.js +22 -9
- package/dist/components/ticketReferences/TicketReferences.js.map +1 -1
- package/dist/components/ticketThread/TicketThread.d.ts +10 -6
- package/dist/components/ticketThread/TicketThread.d.ts.map +1 -1
- package/dist/components/ticketThread/TicketThread.js +2 -2
- package/dist/components/ticketThread/TicketThread.js.map +1 -1
- package/package.json +1 -1
- package/src/components/form/engine/CompactToolbar.tsx +8 -3
- package/src/components/form/engine/FormEngine.stories.tsx +206 -13
- package/src/components/form/engine/FormEngine.tsx +38 -8
- package/src/components/form/engine/OptionFieldMessages.tsx +7 -5
- package/src/components/form/engine/variants/VariantCalmTable.tsx +3 -1
- package/src/components/form/engine/variants/variantModel.ts +10 -3
- package/src/components/form/fields/array/ArrayAuto.tsx +10 -1
- package/src/components/form/fields/array/ArrayAutoField.tsx +10 -1
- package/src/components/form/fields/binary/Binary.tsx +10 -0
- package/src/components/form/fields/emittedValues.ts +62 -0
- package/src/components/form/fields/long-string/LongString.tsx +28 -8
- package/src/components/form/fields/number/Number.tsx +9 -0
- package/src/components/form/fields/rich-text/RichText.tsx +10 -0
- package/src/components/supportTicket/InterfaceReferenceTags.stories.tsx +86 -0
- package/src/components/supportTicket/InterfaceReferenceTags.tsx +44 -11
- package/src/components/supportTicket/ReferencePicker.stories.tsx +51 -0
- package/src/components/supportTicket/ReferencePicker.tsx +26 -5
- package/src/components/supportTicket/TicketReplyBox.stories.tsx +5 -2
- package/src/components/supportTicket/__fixtures__/kindMarks.ts +28 -0
- package/src/components/supportTicket/meta.ts +21 -0
- package/src/components/supportTicket/useLoadedImages.ts +73 -0
- package/src/components/ticketReferences/TicketReferences.tsx +27 -7
- package/src/components/ticketThread/TicketThread.tsx +11 -3
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { ReqoreControlGroup, ReqoreP, ReqoreTag } from '@qoretechnologies/reqore';
|
|
2
2
|
import { TSizes } from '@qoretechnologies/reqore/dist/constants/sizes';
|
|
3
|
-
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
4
3
|
import { ComponentProps } from 'react';
|
|
5
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
defaultInterfaceIcon,
|
|
6
|
+
IInterfaceReference,
|
|
7
|
+
toInterfaceKindMark,
|
|
8
|
+
TResolveInterfaceIcon,
|
|
9
|
+
} from './meta';
|
|
10
|
+
import { useLoadedImages } from './useLoadedImages';
|
|
6
11
|
|
|
7
12
|
export interface IInterfaceReferenceTagsProps {
|
|
8
13
|
references?: IInterfaceReference[];
|
|
9
14
|
/** Optional leading label, e.g. "References" in a ticket header. Omitted (the
|
|
10
15
|
* default) inside the thread, where the chips sit under a message. */
|
|
11
16
|
label?: string;
|
|
12
|
-
/** Resolve a per-kind icon
|
|
17
|
+
/** Resolve a per-kind icon: a font icon, or a mark `{ icon, image }` whose image
|
|
18
|
+
* (e.g. the Qog logo) is drawn once it loads, with the font icon before that and
|
|
19
|
+
* instead of it if it fails. Consumers with their own icon vocabulary (the IDE)
|
|
13
20
|
* pass one; when omitted the built-in per-kind default is used. */
|
|
14
|
-
resolveInterfaceIcon?:
|
|
21
|
+
resolveInterfaceIcon?: TResolveInterfaceIcon;
|
|
15
22
|
/** Open a referenced interface; chips are static (informational) when omitted —
|
|
16
23
|
* e.g. the staff view, which can't reach the customer's instance. */
|
|
17
24
|
onInterfaceClick?: (reference: IInterfaceReference) => void;
|
|
@@ -32,6 +39,21 @@ export interface IInterfaceReferenceTagsProps {
|
|
|
32
39
|
* qogs list does. When this (or the reference's own `logo`) returns a value, the
|
|
33
40
|
* chip shows the image in place of the kind icon. */
|
|
34
41
|
resolveInterfaceImage?: (reference: IInterfaceReference) => string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Which image a chip draws when there is more than one:
|
|
44
|
+
*
|
|
45
|
+
* - `false` (the default): the reference's own image first — its stored `logo`,
|
|
46
|
+
* then `resolveInterfaceImage` — and the kind's image (from
|
|
47
|
+
* `resolveInterfaceIcon`) only when it has neither. This is how chips have
|
|
48
|
+
* always looked: an app-triggered Qog shows its trigger app's logo.
|
|
49
|
+
* - `true`: the kind's image first, then the reference's own. For a host that
|
|
50
|
+
* draws every interface of a kind with one mark (the IDE's green Qog), so a
|
|
51
|
+
* Qog reads as a Qog whatever app triggers it.
|
|
52
|
+
*
|
|
53
|
+
* Either way the kind's font icon shows until the chosen image loads, and
|
|
54
|
+
* instead of it if it cannot.
|
|
55
|
+
*/
|
|
56
|
+
preferKindImage?: boolean;
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
/**
|
|
@@ -51,11 +73,22 @@ export const InterfaceReferenceTags = ({
|
|
|
51
73
|
intent,
|
|
52
74
|
customTheme,
|
|
53
75
|
resolveInterfaceImage,
|
|
76
|
+
preferKindImage = false,
|
|
54
77
|
}: IInterfaceReferenceTagsProps) => {
|
|
78
|
+
const resolveIcon = resolveInterfaceIcon ?? defaultInterfaceIcon;
|
|
79
|
+
const marks = (references ?? []).map((reference) => {
|
|
80
|
+
const mark = toInterfaceKindMark(resolveIcon(reference.interface_kind));
|
|
81
|
+
const own = reference.logo ?? resolveInterfaceImage?.(reference);
|
|
82
|
+
return {
|
|
83
|
+
reference,
|
|
84
|
+
icon: mark.icon,
|
|
85
|
+
image: preferKindImage ? (mark.image ?? own) : (own ?? mark.image),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
const isLoaded = useLoadedImages(marks.map(({ image }) => image));
|
|
55
89
|
if (!references?.length) {
|
|
56
90
|
return null;
|
|
57
91
|
}
|
|
58
|
-
const resolveIcon = resolveInterfaceIcon ?? defaultInterfaceIcon;
|
|
59
92
|
return (
|
|
60
93
|
<ReqoreControlGroup verticalAlign='center' wrap gapSize='small'>
|
|
61
94
|
{label ? (
|
|
@@ -63,18 +96,18 @@ export const InterfaceReferenceTags = ({
|
|
|
63
96
|
{label}
|
|
64
97
|
</ReqoreP>
|
|
65
98
|
) : null}
|
|
66
|
-
{
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
const
|
|
99
|
+
{marks.map(({ reference, icon, image }) => {
|
|
100
|
+
// The chosen image (see `preferKindImage`) once it has loaded; the kind's
|
|
101
|
+
// font icon until then, and if it fails.
|
|
102
|
+
const loaded = isLoaded(image);
|
|
70
103
|
return (
|
|
71
104
|
<ReqoreTag
|
|
72
105
|
key={
|
|
73
106
|
reference.reference_id ??
|
|
74
107
|
`${reference.interface_kind}:${reference.interface_name}`
|
|
75
108
|
}
|
|
76
|
-
icon={
|
|
77
|
-
leftIconProps={
|
|
109
|
+
icon={icon}
|
|
110
|
+
leftIconProps={loaded ? { image } : undefined}
|
|
78
111
|
labelKey={reference.interface_kind}
|
|
79
112
|
label={reference.interface_name}
|
|
80
113
|
size={size}
|
|
@@ -4,6 +4,13 @@ import { ComponentProps, useState } from 'react';
|
|
|
4
4
|
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
5
5
|
import { StoryMeta } from '../../types';
|
|
6
6
|
import { InterfaceReferenceTags } from './InterfaceReferenceTags';
|
|
7
|
+
import {
|
|
8
|
+
STORY_BROKEN_IMAGE,
|
|
9
|
+
STORY_QOG_IMAGE,
|
|
10
|
+
storyBrokenKindMark,
|
|
11
|
+
storyKindMark,
|
|
12
|
+
} from './__fixtures__/kindMarks';
|
|
13
|
+
import { whenImageSettled } from './useLoadedImages';
|
|
7
14
|
import { IInterfaceReference } from './meta';
|
|
8
15
|
import { ReferencePicker, TReferencePickerItem } from './ReferencePicker';
|
|
9
16
|
import { TicketReplyBox } from './TicketReplyBox';
|
|
@@ -517,3 +524,47 @@ export const AboveTheComposer: Story = {
|
|
|
517
524
|
await expect(canvas.getAllByText('order-sync:1.2').length).toBeGreaterThan(0);
|
|
518
525
|
},
|
|
519
526
|
};
|
|
527
|
+
|
|
528
|
+
const menuImages = (canvasElement: HTMLElement) =>
|
|
529
|
+
Array.from(canvasElement.querySelectorAll('.reqore-menu-item img')).map((img) =>
|
|
530
|
+
img.getAttribute('src')
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
export const KindImage: Story = {
|
|
534
|
+
parameters: {
|
|
535
|
+
docs: {
|
|
536
|
+
description: {
|
|
537
|
+
story:
|
|
538
|
+
'Renders the picker with a host resolver that gives Qogs an image: the Qogs kind and each Qog in the list show the green Qog mark; every other kind keeps its font icon.',
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
decorators: [darkTheme],
|
|
543
|
+
render: () => <Hosted initialKind='qog' resolveInterfaceIcon={storyKindMark} />,
|
|
544
|
+
async play({ canvasElement }) {
|
|
545
|
+
// the Qogs kind row plus its four interfaces
|
|
546
|
+
await waitFor(() =>
|
|
547
|
+
expect(menuImages(canvasElement).filter((src) => src === STORY_QOG_IMAGE)).toHaveLength(5)
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
export const KindImageFailsToLoad: Story = {
|
|
553
|
+
parameters: {
|
|
554
|
+
docs: {
|
|
555
|
+
description: {
|
|
556
|
+
story:
|
|
557
|
+
'Renders the picker with a Qog image that cannot load: the Qogs kind and its interfaces keep the font icon instead of a broken picture.',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
decorators: [darkTheme],
|
|
562
|
+
render: () => <Hosted initialKind='qog' resolveInterfaceIcon={storyBrokenKindMark} />,
|
|
563
|
+
async play({ canvasElement }) {
|
|
564
|
+
const canvas = within(canvasElement);
|
|
565
|
+
await canvas.findByRole('button', { name: 'telegram-intake' });
|
|
566
|
+
// Once the load has failed, no picture is drawn.
|
|
567
|
+
await whenImageSettled(STORY_BROKEN_IMAGE);
|
|
568
|
+
await expect(menuImages(canvasElement)).toEqual([]);
|
|
569
|
+
},
|
|
570
|
+
};
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
ReqoreSpinner,
|
|
7
7
|
} from '@qoretechnologies/reqore';
|
|
8
8
|
import { TReqoreBadge } from '@qoretechnologies/reqore/dist/components/Button';
|
|
9
|
-
import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons';
|
|
10
9
|
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
10
|
import styled from 'styled-components';
|
|
12
11
|
import { thinScrollbar } from '../../helpers/scrollbar';
|
|
@@ -15,7 +14,10 @@ import {
|
|
|
15
14
|
defaultInterfaceIcon,
|
|
16
15
|
defaultInterfaceKindLabel,
|
|
17
16
|
IInterfaceReference,
|
|
17
|
+
toInterfaceKindMark,
|
|
18
|
+
TResolveInterfaceIcon,
|
|
18
19
|
} from './meta';
|
|
20
|
+
import { useLoadedImages } from './useLoadedImages';
|
|
19
21
|
|
|
20
22
|
/*
|
|
21
23
|
* The interface browser: kinds on one side, the selected kind's interfaces on the
|
|
@@ -227,9 +229,11 @@ export interface IReferencePickerProps {
|
|
|
227
229
|
onRemove?: (reference: IInterfaceReference) => void;
|
|
228
230
|
/** the whole picker is inert (e.g. a reply is in flight) */
|
|
229
231
|
disabled?: boolean;
|
|
230
|
-
/** Per-kind icon
|
|
232
|
+
/** Per-kind icon: a font icon, or a mark `{ icon, image }` whose image (e.g. the Qog
|
|
233
|
+
* logo) is drawn once it loads, with the font icon before that and instead of it if
|
|
234
|
+
* it fails. Consumers with their own icon vocabulary (the IDE) pass one; the
|
|
231
235
|
* built-in per-kind default is used when omitted. */
|
|
232
|
-
resolveInterfaceIcon?:
|
|
236
|
+
resolveInterfaceIcon?: TResolveInterfaceIcon;
|
|
233
237
|
/**
|
|
234
238
|
* Per-kind reading name — "Value maps" for `value-map`. The kind id is a wire value
|
|
235
239
|
* and never reaches the user; this is what does. Search matches both, so typing
|
|
@@ -401,11 +405,28 @@ export const ReferencePicker = ({
|
|
|
401
405
|
still own the scroll and the layout; the menus render transparent inside them.
|
|
402
406
|
Only menu items go inside a `ReqoreMenu`: it clones every child with its own props,
|
|
403
407
|
so a spinner or a note nested in one would be handed props it can't take. */
|
|
408
|
+
/* A kind's icon on a menu item: its font icon, and its image (the Qog logo) once
|
|
409
|
+
that has loaded — never a broken picture while it loads or if it fails. */
|
|
410
|
+
const kindMarks = new Map(
|
|
411
|
+
Array.from(new Set([...visibleKinds, kind].filter(Boolean) as string[])).map((option) => [
|
|
412
|
+
option,
|
|
413
|
+
toInterfaceKindMark(resolveInterfaceIcon(option)),
|
|
414
|
+
])
|
|
415
|
+
);
|
|
416
|
+
const isImageLoaded = useLoadedImages(Array.from(kindMarks.values()).map((mark) => mark.image));
|
|
417
|
+
const kindIconProps = (option: string) => {
|
|
418
|
+
const mark = kindMarks.get(option) ?? toInterfaceKindMark(resolveInterfaceIcon(option));
|
|
419
|
+
return {
|
|
420
|
+
icon: mark.icon,
|
|
421
|
+
leftIconProps: isImageLoaded(mark.image) ? { image: mark.image } : undefined,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
|
|
404
425
|
const kindItems = visibleKinds.map((option) => (
|
|
405
426
|
<ReqoreMenuItem
|
|
406
427
|
key={option}
|
|
407
428
|
className={matchesKind(option) ? undefined : DIMMED_CLASS}
|
|
408
|
-
|
|
429
|
+
{...kindIconProps(option)}
|
|
409
430
|
label={resolveKindLabel(option)}
|
|
410
431
|
selected={option === kind}
|
|
411
432
|
disabled={disabled}
|
|
@@ -452,7 +473,7 @@ export const ReferencePicker = ({
|
|
|
452
473
|
<ReqoreMenuItem
|
|
453
474
|
key={name}
|
|
454
475
|
className={isPicked ? PICKED_CLASS : undefined}
|
|
455
|
-
|
|
476
|
+
{...kindIconProps(kind)}
|
|
456
477
|
rightIcon={isPicked ? 'CheckLine' : 'AddLine'}
|
|
457
478
|
label={name}
|
|
458
479
|
description={description}
|
|
@@ -284,8 +284,11 @@ export const QogTriggerAppLogo: Story = {
|
|
|
284
284
|
async play({ canvasElement }) {
|
|
285
285
|
const canvas = within(canvasElement);
|
|
286
286
|
await expect(canvas.getByText('telegram-intake')).toBeInTheDocument();
|
|
287
|
-
// the trigger-app / stored-logo qogs render their app logo image
|
|
288
|
-
|
|
287
|
+
// the trigger-app / stored-logo qogs render their app logo image once it has
|
|
288
|
+
// loaded (the qog glyph stands in until then)
|
|
289
|
+
await waitFor(() =>
|
|
290
|
+
expect(canvasElement.querySelectorAll('.reqore-tag img').length).toBe(3)
|
|
291
|
+
);
|
|
289
292
|
},
|
|
290
293
|
};
|
|
291
294
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defaultInterfaceIcon, TInterfaceKindIcon } from '../meta';
|
|
2
|
+
|
|
3
|
+
/* Stand-ins for a host's kind marks in stories. Inline, so they load offline. */
|
|
4
|
+
|
|
5
|
+
/** A green Qog-like mark: loads. */
|
|
6
|
+
export const STORY_QOG_IMAGE = `data:image/svg+xml;utf8,${encodeURIComponent(
|
|
7
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#2e9d4f"/><circle cx="12" cy="12" r="5" fill="#fff"/></svg>'
|
|
8
|
+
)}`;
|
|
9
|
+
|
|
10
|
+
/** A trigger app's logo: loads. */
|
|
11
|
+
export const STORY_APP_LOGO = `data:image/svg+xml;utf8,${encodeURIComponent(
|
|
12
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="12" fill="#229ed9"/></svg>'
|
|
13
|
+
)}`;
|
|
14
|
+
|
|
15
|
+
/** An image that can never load (malformed data URI). */
|
|
16
|
+
export const STORY_BROKEN_IMAGE = 'data:image/png;base64,not-an-image';
|
|
17
|
+
|
|
18
|
+
/** The IDE's resolver: a Qog is its green mark (font icon fallback); the rest, font icons. */
|
|
19
|
+
export const storyKindMark = (kind: string): TInterfaceKindIcon =>
|
|
20
|
+
kind === 'fsm' || kind === 'qog' ?
|
|
21
|
+
{ icon: 'FlowChart', image: STORY_QOG_IMAGE }
|
|
22
|
+
: defaultInterfaceIcon(kind);
|
|
23
|
+
|
|
24
|
+
/** The same, with a Qog image that fails to load. */
|
|
25
|
+
export const storyBrokenKindMark = (kind: string): TInterfaceKindIcon =>
|
|
26
|
+
kind === 'fsm' || kind === 'qog' ?
|
|
27
|
+
{ icon: 'FlowChart', image: STORY_BROKEN_IMAGE }
|
|
28
|
+
: defaultInterfaceIcon(kind);
|
|
@@ -144,6 +144,27 @@ const DEFAULT_INTERFACE_ICONS: Record<string, IReqoreIconName> = {
|
|
|
144
144
|
export const defaultInterfaceIcon = (kind: string): IReqoreIconName =>
|
|
145
145
|
DEFAULT_INTERFACE_ICONS[kind] ?? 'CodeBoxLine';
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* An interface kind's mark: a font icon, and optionally an image (e.g. the green
|
|
149
|
+
* Qog logo). The font icon is what shows while the image loads, and instead of it
|
|
150
|
+
* when there is none or it cannot load. Same shape as qorus-ide's
|
|
151
|
+
* `interfaceKindMark()`, so the IDE can return that directly.
|
|
152
|
+
*/
|
|
153
|
+
export interface IInterfaceKindMark {
|
|
154
|
+
icon: IReqoreIconName;
|
|
155
|
+
image?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** What a `resolveInterfaceIcon` may return: a font icon, or a mark with an image. */
|
|
159
|
+
export type TInterfaceKindIcon = IReqoreIconName | IInterfaceKindMark;
|
|
160
|
+
|
|
161
|
+
/** Resolve the icon for an interface kind (see `TInterfaceKindIcon`). */
|
|
162
|
+
export type TResolveInterfaceIcon = (kind: string) => TInterfaceKindIcon;
|
|
163
|
+
|
|
164
|
+
/** A resolver's answer as a mark, so callers handle one shape. */
|
|
165
|
+
export const toInterfaceKindMark = (icon: TInterfaceKindIcon): IInterfaceKindMark =>
|
|
166
|
+
typeof icon === 'string' ? { icon } : icon;
|
|
167
|
+
|
|
147
168
|
// The reading name for each interface kind. The kind id (`value-map`, `ai-collection`)
|
|
148
169
|
// is a wire value — it's what a host fetches with and what gets stored on the reference
|
|
149
170
|
// — so it must never be what the user reads. These are plural because they name a
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/* What is known about each image URL, shared by every surface for the life of
|
|
4
|
+
the page: an image that loaded once is drawn at once the next time, and one
|
|
5
|
+
that failed is not tried again on every render. */
|
|
6
|
+
const imageStatus = new Map<string, 'loaded' | 'failed'>();
|
|
7
|
+
const pending = new Map<string, Promise<void>>();
|
|
8
|
+
|
|
9
|
+
const loadImage = (src: string): Promise<void> => {
|
|
10
|
+
let promise = pending.get(src);
|
|
11
|
+
if (!promise) {
|
|
12
|
+
promise = new Promise<void>((resolve) => {
|
|
13
|
+
const image = new Image();
|
|
14
|
+
image.onload = () => {
|
|
15
|
+
imageStatus.set(src, 'loaded');
|
|
16
|
+
resolve();
|
|
17
|
+
};
|
|
18
|
+
image.onerror = () => {
|
|
19
|
+
imageStatus.set(src, 'failed');
|
|
20
|
+
resolve();
|
|
21
|
+
};
|
|
22
|
+
image.src = src;
|
|
23
|
+
}).finally(() => pending.delete(src));
|
|
24
|
+
pending.set(src, promise);
|
|
25
|
+
}
|
|
26
|
+
return promise;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Which of `sources` have loaded. An icon that has an image draws its font icon
|
|
31
|
+
* until this says the image loaded, and keeps the font icon when it fails — the
|
|
32
|
+
* image is never drawn as a broken picture or an empty box.
|
|
33
|
+
*
|
|
34
|
+
* One hook for a whole list, so a component that maps over references (or a
|
|
35
|
+
* `ReqoreMenu`, which clones its children and so cannot hold per-item hooks)
|
|
36
|
+
* asks once.
|
|
37
|
+
*/
|
|
38
|
+
export const useLoadedImages = (sources: Array<string | undefined>): ((src?: string) => boolean) => {
|
|
39
|
+
const [, setVersion] = useState(0);
|
|
40
|
+
const key = sources.filter(Boolean).join('\u0000');
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let active = true;
|
|
44
|
+
const unknown = Array.from(new Set(sources.filter((src): src is string => !!src))).filter(
|
|
45
|
+
(src) => !imageStatus.has(src)
|
|
46
|
+
);
|
|
47
|
+
unknown.forEach((src) => {
|
|
48
|
+
loadImage(src).then(() => {
|
|
49
|
+
if (active) {
|
|
50
|
+
setVersion((version) => version + 1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
active = false;
|
|
56
|
+
};
|
|
57
|
+
}, [key]);
|
|
58
|
+
|
|
59
|
+
return (src?: string) => !!src && imageStatus.get(src) === 'loaded';
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolves once `src` has loaded or failed — so a test or story can check the
|
|
64
|
+
* settled state instead of waiting an arbitrary time.
|
|
65
|
+
*/
|
|
66
|
+
export const whenImageSettled = (src: string): Promise<void> =>
|
|
67
|
+
imageStatus.has(src) ? Promise.resolve() : loadImage(src);
|
|
68
|
+
|
|
69
|
+
/** Forget every image's status. For tests only. */
|
|
70
|
+
export const resetLoadedImagesForTests = (): void => {
|
|
71
|
+
imageStatus.clear();
|
|
72
|
+
pending.clear();
|
|
73
|
+
};
|
|
@@ -23,8 +23,11 @@ import { ImageLightbox } from '../imageLightbox';
|
|
|
23
23
|
import {
|
|
24
24
|
defaultInterfaceIcon,
|
|
25
25
|
IInterfaceReference,
|
|
26
|
+
toInterfaceKindMark,
|
|
27
|
+
TResolveInterfaceIcon,
|
|
26
28
|
TTicketViewerRole,
|
|
27
29
|
} from '../supportTicket/meta';
|
|
30
|
+
import { useLoadedImages } from '../supportTicket/useLoadedImages';
|
|
28
31
|
import type { ITicketThreadAttachment, ITicketThreadMessage } from '../ticketThread/TicketThread';
|
|
29
32
|
|
|
30
33
|
/*
|
|
@@ -74,8 +77,13 @@ export interface ITicketReferencesProps {
|
|
|
74
77
|
* Omitted ⇒ the action is not offered. It is also omitted per-row for anything filed
|
|
75
78
|
* against the ticket itself rather than a reply, which has no message to reveal. */
|
|
76
79
|
onShowInChat?: (messageId: string) => void;
|
|
77
|
-
/** resolve a per-kind icon for a referenced interface (the IDE passes its own
|
|
78
|
-
|
|
80
|
+
/** resolve a per-kind icon for a referenced interface (the IDE passes its own
|
|
81
|
+
* vocabulary): a font icon, or a mark `{ icon, image }` whose image is drawn once
|
|
82
|
+
* it loads, with the font icon before that and instead of it if it fails */
|
|
83
|
+
resolveInterfaceIcon?: TResolveInterfaceIcon;
|
|
84
|
+
/** Draw the kind's image ahead of a reference's own logo; see `preferKindImage` on
|
|
85
|
+
* `InterfaceReferenceTags`. Off by default. */
|
|
86
|
+
preferKindImage?: boolean;
|
|
79
87
|
loading?: boolean;
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -415,6 +423,7 @@ export const TicketReferences = ({
|
|
|
415
423
|
onInterfaceClick,
|
|
416
424
|
onShowInChat,
|
|
417
425
|
resolveInterfaceIcon,
|
|
426
|
+
preferKindImage = false,
|
|
418
427
|
loading,
|
|
419
428
|
}: ITicketReferencesProps) => {
|
|
420
429
|
// references: ticket-level + every message's, deduped. Rendered as rows carrying
|
|
@@ -433,6 +442,17 @@ export const TicketReferences = ({
|
|
|
433
442
|
[references, messages]
|
|
434
443
|
);
|
|
435
444
|
const resolveIcon = resolveInterfaceIcon ?? defaultInterfaceIcon;
|
|
445
|
+
/* Each reference's icon: the kind's font icon, and the image chosen by the same
|
|
446
|
+
precedence as the chips (`preferKindImage`), drawn once it has loaded. */
|
|
447
|
+
const markOf = (reference: IInterfaceReference) => {
|
|
448
|
+
const mark = toInterfaceKindMark(resolveIcon(reference.interface_kind));
|
|
449
|
+
const own = reference.logo;
|
|
450
|
+
return {
|
|
451
|
+
icon: mark.icon,
|
|
452
|
+
image: preferKindImage ? (mark.image ?? own) : (own ?? mark.image),
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
const isImageLoaded = useLoadedImages(allRefs.map((reference) => markOf(reference).image));
|
|
436
456
|
|
|
437
457
|
/**
|
|
438
458
|
* The trailing controls for a row: its primary action, plus a "three dots" menu when
|
|
@@ -768,17 +788,17 @@ export const TicketReferences = ({
|
|
|
768
788
|
the group's size reaches every row (ControlGroup/index.tsx:436) */}
|
|
769
789
|
<ReqoreControlGroup vertical fluid gapSize='small' size='small'>
|
|
770
790
|
{visibleRefs.map((reference) => {
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
const image = reference
|
|
791
|
+
// the same vocabulary and precedence as the reference chips: the kind's
|
|
792
|
+
// font icon, and the chosen image once it has loaded
|
|
793
|
+
const { icon, image } = markOf(reference);
|
|
774
794
|
return (
|
|
775
795
|
<ReqoreEntityRow
|
|
776
796
|
key={
|
|
777
797
|
reference.reference_id ??
|
|
778
798
|
`${reference.interface_kind}:${reference.interface_name}`
|
|
779
799
|
}
|
|
780
|
-
icon={
|
|
781
|
-
iconImage={image}
|
|
800
|
+
icon={icon}
|
|
801
|
+
iconImage={isImageLoaded(image) ? image : undefined}
|
|
782
802
|
label={reference.interface_name}
|
|
783
803
|
metadata={
|
|
784
804
|
<ReqoreTextEffect effect={META_EFFECT}>
|
|
@@ -21,6 +21,7 @@ import { ImageLightbox } from '../imageLightbox';
|
|
|
21
21
|
import {
|
|
22
22
|
IInterfaceReference,
|
|
23
23
|
InterfaceReferenceTags,
|
|
24
|
+
TResolveInterfaceIcon,
|
|
24
25
|
TTicketMessageAuthor,
|
|
25
26
|
TTicketViewerRole,
|
|
26
27
|
} from '../supportTicket';
|
|
@@ -84,10 +85,15 @@ export interface ITicketThreadProps {
|
|
|
84
85
|
* where a consumer hasn't wired it. Non-image attachments never use it.
|
|
85
86
|
*/
|
|
86
87
|
fetchAttachmentUrl?: (attachmentId: string) => Promise<string>;
|
|
87
|
-
/** resolve a per-kind icon for a referenced interface
|
|
88
|
-
* icon
|
|
88
|
+
/** resolve a per-kind icon for a referenced interface: a font icon, or a mark
|
|
89
|
+
* `{ icon, image }` whose image (e.g. the Qog logo) is drawn once it loads, with
|
|
90
|
+
* the font icon before that and instead of it if it fails. Consumers with their
|
|
91
|
+
* own icon vocabulary (e.g. the qorus-ide IDE) pass one; when omitted, a built-in
|
|
89
92
|
* per-kind default is used so every surface shows consistent chips. */
|
|
90
|
-
resolveInterfaceIcon?:
|
|
93
|
+
resolveInterfaceIcon?: TResolveInterfaceIcon;
|
|
94
|
+
/** Draw the kind's image ahead of a reference's own logo on the reference chips;
|
|
95
|
+
* see `preferKindImage` on `InterfaceReferenceTags`. Off by default. */
|
|
96
|
+
preferKindImage?: boolean;
|
|
91
97
|
/** open a referenced interface; reference chips are static when omitted (e.g. the staff
|
|
92
98
|
* view, which can't reach the customer's instance, leaves this out). */
|
|
93
99
|
onInterfaceClick?: (reference: IInterfaceReference) => void;
|
|
@@ -557,6 +563,7 @@ export const TicketThread = ({
|
|
|
557
563
|
onDownloadAttachment,
|
|
558
564
|
fetchAttachmentUrl,
|
|
559
565
|
resolveInterfaceIcon,
|
|
566
|
+
preferKindImage,
|
|
560
567
|
onInterfaceClick,
|
|
561
568
|
ticketReferences,
|
|
562
569
|
}: ITicketThreadProps) => {
|
|
@@ -694,6 +701,7 @@ export const TicketThread = ({
|
|
|
694
701
|
customTheme={ACCENT}
|
|
695
702
|
references={refsFor(message)}
|
|
696
703
|
resolveInterfaceIcon={resolveInterfaceIcon}
|
|
704
|
+
preferKindImage={preferKindImage}
|
|
697
705
|
onInterfaceClick={onInterfaceClick}
|
|
698
706
|
/>
|
|
699
707
|
</div>
|