@nyaruka/temba-components 0.162.0 → 0.164.0
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/CHANGELOG.md +24 -0
- package/dist/temba-components.js +1865 -686
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +29 -9
- package/src/display/Lightbox.ts +57 -109
- package/src/display/Thumbnail.ts +34 -7
- package/src/flow/FlowSearch.ts +66 -2
- package/src/flow/NodeEditor.ts +109 -1
- package/src/interfaces.ts +79 -2
- package/src/layout/Card.ts +311 -0
- package/src/layout/CardLayout.ts +425 -0
- package/src/layout/CardStack.ts +131 -0
- package/src/layout/Dialog.ts +84 -24
- package/src/layout/HeaderBar.ts +38 -0
- package/src/layout/PageHeader.ts +5 -6
- package/src/layout/Resizer.ts +20 -0
- package/src/list/CampaignList.ts +144 -0
- package/src/list/ContactList.ts +3 -1
- package/src/list/FlowList.ts +31 -7
- package/src/list/MsgList.ts +18 -12
- package/src/list/SortableList.ts +45 -6
- package/src/list/TriggerList.ts +538 -0
- package/src/live/CampaignEvents.ts +1094 -0
- package/src/live/ContactDetails.ts +3 -4
- package/src/live/ContactFields.ts +1 -1
- package/src/live/ContactNameFetch.ts +5 -2
- package/src/live/ContactNotepad.ts +88 -2
- package/src/live/ContactStoreElement.ts +12 -2
- package/src/live/ContactTimeline.ts +5 -1
- package/src/utils.ts +19 -0
- package/temba-modules.ts +14 -0
- package/web-dev-server.config.mjs +54 -0
package/package.json
CHANGED
package/src/display/Chat.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { RapidElement } from '../RapidElement';
|
|
|
5
5
|
import { CustomEventType } from '../interfaces';
|
|
6
6
|
import { TicketEvent } from '../events';
|
|
7
7
|
import { DEFAULT_AVATAR } from '../webchat/assets';
|
|
8
|
+
import { attachmentAsString } from '../utils';
|
|
8
9
|
|
|
9
10
|
const BATCH_TIME_WINDOW = 60 * 60 * 1000;
|
|
10
11
|
const SCROLL_FETCH_BUFFER = 200; // pixels from top
|
|
@@ -113,6 +114,13 @@ export interface MsgEvent extends ContactEvent {
|
|
|
113
114
|
_logs_url?: string;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// whether a message has nothing to show in its bubble (and isn't deleted)
|
|
118
|
+
const isEmptyMsg = (event: MsgEvent): boolean =>
|
|
119
|
+
!event._deleted &&
|
|
120
|
+
!!event.msg &&
|
|
121
|
+
!event.msg.text &&
|
|
122
|
+
(event.msg.attachments || []).length === 0;
|
|
123
|
+
|
|
116
124
|
const TIME_FORMAT = { hour: 'numeric', minute: '2-digit' } as any;
|
|
117
125
|
const VERBOSE_FORMAT = {
|
|
118
126
|
weekday: undefined,
|
|
@@ -351,13 +359,15 @@ export class Chat extends RapidElement {
|
|
|
351
359
|
color: rgba(192, 24, 41, 0.6);
|
|
352
360
|
}
|
|
353
361
|
|
|
354
|
-
.deleted .bubble
|
|
362
|
+
.deleted .bubble,
|
|
363
|
+
.empty .bubble {
|
|
355
364
|
background: #fff;
|
|
356
365
|
color: #999;
|
|
357
366
|
border: 1px solid #e0e0e0;
|
|
358
367
|
}
|
|
359
368
|
|
|
360
|
-
.deleted .bubble .name
|
|
369
|
+
.deleted .bubble .name,
|
|
370
|
+
.empty .bubble .name {
|
|
361
371
|
color: #aaa;
|
|
362
372
|
}
|
|
363
373
|
|
|
@@ -369,7 +379,8 @@ export class Chat extends RapidElement {
|
|
|
369
379
|
font-size: 13px;
|
|
370
380
|
}
|
|
371
381
|
|
|
372
|
-
.message-deleted
|
|
382
|
+
.message-deleted,
|
|
383
|
+
.message-empty {
|
|
373
384
|
font-style: italic;
|
|
374
385
|
margin-bottom: 0;
|
|
375
386
|
line-height: 1.2;
|
|
@@ -1325,13 +1336,14 @@ export class Chat extends RapidElement {
|
|
|
1325
1336
|
(statusClass === 'failed' || statusClass === 'errored'));
|
|
1326
1337
|
const unsendableClass = hasError ? 'error' : '';
|
|
1327
1338
|
const deletedClass = msgEvent._deleted ? 'deleted' : '';
|
|
1339
|
+
const emptyClass = isEmptyMsg(msgEvent) ? 'empty' : '';
|
|
1328
1340
|
const latestClass = index === msgIds.length - 1 ? 'latest' : '';
|
|
1329
1341
|
const eventClass = msg._rendered ? 'is-event' : '';
|
|
1330
1342
|
const noteClass = msg.type === 'ticket_note_added' ? 'note' : '';
|
|
1331
1343
|
const matchClass =
|
|
1332
1344
|
this.highlightMessageUuid === msg.uuid ? 'search-match' : '';
|
|
1333
1345
|
return html`<div
|
|
1334
|
-
class="row message ${statusClass} ${unsendableClass} ${deletedClass} ${latestClass} ${eventClass} ${noteClass} ${matchClass}"
|
|
1346
|
+
class="row message ${statusClass} ${unsendableClass} ${deletedClass} ${emptyClass} ${latestClass} ${eventClass} ${noteClass} ${matchClass}"
|
|
1335
1347
|
data-uuid=${msg.uuid || nothing}
|
|
1336
1348
|
>
|
|
1337
1349
|
${this.renderMessage(
|
|
@@ -1430,9 +1442,12 @@ export class Chat extends RapidElement {
|
|
|
1430
1442
|
: message._deleted.user?.name || 'user'
|
|
1431
1443
|
: null;
|
|
1432
1444
|
|
|
1445
|
+
// messages with no text and no attachments get a muted placeholder
|
|
1446
|
+
const isEmpty = isEmptyMsg(message);
|
|
1447
|
+
|
|
1433
1448
|
// check if message has location attachment and text is just coordinates
|
|
1434
1449
|
const hasLocationAttachment = message.msg.attachments?.some((att) =>
|
|
1435
|
-
att.startsWith('geo:')
|
|
1450
|
+
attachmentAsString(att).startsWith('geo:')
|
|
1436
1451
|
);
|
|
1437
1452
|
const textIsCoordinates =
|
|
1438
1453
|
hasLocationAttachment &&
|
|
@@ -1480,18 +1495,23 @@ export class Chat extends RapidElement {
|
|
|
1480
1495
|
Message deleted by ${deletedByText}
|
|
1481
1496
|
</div>
|
|
1482
1497
|
</div>`
|
|
1483
|
-
:
|
|
1498
|
+
: isEmpty
|
|
1484
1499
|
? html`<div class="bubble">
|
|
1485
1500
|
${name ? html`<div class="name">${name}</div>` : null}
|
|
1486
|
-
<div class="message-
|
|
1501
|
+
<div class="message-empty">Empty Message</div>
|
|
1487
1502
|
</div>`
|
|
1488
|
-
:
|
|
1503
|
+
: message.msg.text && !textIsCoordinates
|
|
1504
|
+
? html`<div class="bubble">
|
|
1505
|
+
${name ? html`<div class="name">${name}</div>` : null}
|
|
1506
|
+
<div class="message-text">${messageText}</div>
|
|
1507
|
+
</div>`
|
|
1508
|
+
: null}
|
|
1489
1509
|
|
|
1490
1510
|
<div class="attachments">
|
|
1491
1511
|
${(message.msg.attachments || []).map(
|
|
1492
1512
|
(attachment) =>
|
|
1493
1513
|
html`<temba-thumbnail
|
|
1494
|
-
attachment="${attachment}"
|
|
1514
|
+
attachment="${attachmentAsString(attachment)}"
|
|
1495
1515
|
></temba-thumbnail>`
|
|
1496
1516
|
)}
|
|
1497
1517
|
</div>
|
package/src/display/Lightbox.ts
CHANGED
|
@@ -1,59 +1,65 @@
|
|
|
1
1
|
import { css, html, PropertyValueMap } from 'lit';
|
|
2
|
-
import { property } from 'lit/decorators.js';
|
|
2
|
+
import { property, state } from 'lit/decorators.js';
|
|
3
3
|
import { RapidElement } from '../RapidElement';
|
|
4
4
|
import { getClasses } from '../utils';
|
|
5
|
-
import { styleMap } from 'lit-html/directives/style-map.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* A full-screen viewer for an image attachment. `showElement` is handed the
|
|
8
|
+
* element that was clicked (a raw <img> or a temba-thumbnail exposing a `url`),
|
|
9
|
+
* and we present that image centered and contained within the viewport with a
|
|
10
|
+
* short fade / scale-in. Clicking anywhere dismisses it.
|
|
11
11
|
*/
|
|
12
12
|
export class Lightbox extends RapidElement {
|
|
13
13
|
static get styles() {
|
|
14
14
|
return css`
|
|
15
15
|
:host {
|
|
16
|
-
|
|
17
|
-
position: absolute;
|
|
16
|
+
position: fixed;
|
|
18
17
|
top: 0;
|
|
19
18
|
left: 0;
|
|
19
|
+
z-index: 10000;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/* a full-viewport layer that centers the image, catches the dismiss
|
|
23
|
+
click, and lays a light translucent mask over the page behind */
|
|
24
|
+
.backdrop {
|
|
25
|
+
position: fixed;
|
|
26
|
+
inset: 0;
|
|
23
27
|
display: flex;
|
|
24
|
-
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
25
30
|
background: rgba(0, 0, 0, 0.5);
|
|
26
|
-
|
|
27
|
-
height: 100svh;
|
|
28
|
-
width: 100svw;
|
|
31
|
+
opacity: 0;
|
|
29
32
|
pointer-events: none;
|
|
33
|
+
transition: opacity var(--anim) ease;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
.zoom
|
|
36
|
+
.backdrop.zoom {
|
|
33
37
|
opacity: 1;
|
|
34
38
|
pointer-events: auto;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
img {
|
|
42
|
+
/* contain the image within the viewport at any aspect ratio, on
|
|
43
|
+
its own solid backing (so transparent images aren't see-through) */
|
|
44
|
+
box-sizing: border-box;
|
|
45
|
+
max-width: 90vw;
|
|
46
|
+
max-height: 90vh;
|
|
47
|
+
object-fit: contain;
|
|
48
|
+
background: #fff;
|
|
49
|
+
padding: 6px;
|
|
50
|
+
border-radius: calc(var(--curvature) * 1.5);
|
|
51
|
+
box-shadow: 0 0 24px 6px rgba(0, 0, 0, 0.4);
|
|
52
|
+
transform: scale(0.85);
|
|
53
|
+
opacity: 0;
|
|
54
|
+
transition:
|
|
55
|
+
transform var(--anim) ease,
|
|
56
|
+
opacity var(--anim) ease;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
display:flex;
|
|
49
|
-
align-items:center;
|
|
50
|
-
color:#fff;
|
|
51
|
-
padding:0.5em;
|
|
52
|
-
border-radius:var(--curvature);
|
|
53
|
-
background:rgba(0,0,0,0.5);
|
|
59
|
+
.zoom img {
|
|
60
|
+
transform: scale(1);
|
|
61
|
+
opacity: 1;
|
|
54
62
|
}
|
|
55
|
-
|
|
56
|
-
}
|
|
57
63
|
`;
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -66,77 +72,41 @@ export class Lightbox extends RapidElement {
|
|
|
66
72
|
@property({ type: Boolean })
|
|
67
73
|
zoom = false;
|
|
68
74
|
|
|
69
|
-
@
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
private ele: HTMLElement;
|
|
73
|
-
private left: number;
|
|
74
|
-
private top: number;
|
|
75
|
-
private height: number;
|
|
76
|
-
private width: number;
|
|
77
|
-
private scale = 1;
|
|
78
|
-
private xTrans = 0;
|
|
79
|
-
private yTrans = 0;
|
|
75
|
+
@state()
|
|
76
|
+
private url = '';
|
|
80
77
|
|
|
81
78
|
protected updated(
|
|
82
79
|
changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
83
80
|
): void {
|
|
81
|
+
// mount first (opacity 0 / scaled down), then flip to zoom on the next
|
|
82
|
+
// tick so the transition to the visible state actually animates
|
|
84
83
|
if (changed.has('show') && this.show) {
|
|
85
84
|
window.setTimeout(() => {
|
|
86
85
|
this.zoom = true;
|
|
87
86
|
}, 0);
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
// once zoomed out, wait for the fade to finish before unmounting the image
|
|
90
90
|
if (changed.has('zoom') && !this.zoom && this.show) {
|
|
91
91
|
window.setTimeout(() => {
|
|
92
|
-
|
|
92
|
+
// unless a re-open during the fade-out already zoomed us back in
|
|
93
|
+
if (!this.zoom) {
|
|
94
|
+
this.show = false;
|
|
95
|
+
}
|
|
93
96
|
}, this.animationTime);
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
public showElement(ele: HTMLElement) {
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
(this.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.top = bounds.top;
|
|
105
|
-
this.width = bounds.width;
|
|
106
|
-
this.height = bounds.height;
|
|
107
|
-
|
|
108
|
-
this.xTrans = 0;
|
|
109
|
-
this.yTrans = 0;
|
|
110
|
-
this.scale = 1;
|
|
111
|
-
|
|
112
|
-
let desiredWidth = this.width;
|
|
113
|
-
let desiredHeight = this.height;
|
|
114
|
-
let desiredScale = this.scale;
|
|
115
|
-
|
|
116
|
-
const maxHeight = window.innerHeight * this.zoomPct;
|
|
117
|
-
const maxWidth = window.innerWidth * this.zoomPct;
|
|
118
|
-
|
|
119
|
-
// if the width fits, constrain by height
|
|
120
|
-
if (this.width * (maxHeight / this.height) < maxWidth) {
|
|
121
|
-
desiredHeight = window.innerHeight * this.zoomPct;
|
|
122
|
-
desiredScale = desiredHeight / this.height;
|
|
123
|
-
desiredWidth = this.width * desiredScale;
|
|
101
|
+
// the clicked element is either a raw <img> or a temba-thumbnail that
|
|
102
|
+
// exposes the attachment url; take whichever gives us an image source
|
|
103
|
+
this.url = (ele as HTMLImageElement).src || (ele as any).url || '';
|
|
104
|
+
if (this.show) {
|
|
105
|
+
// already mounted (e.g. re-opened mid-dismissal) — re-zoom in place
|
|
106
|
+
this.zoom = true;
|
|
124
107
|
} else {
|
|
125
|
-
|
|
126
|
-
desiredScale = desiredWidth / this.width;
|
|
127
|
-
desiredHeight = this.height * desiredScale;
|
|
108
|
+
this.show = true; // updated() flips zoom on the next tick
|
|
128
109
|
}
|
|
129
|
-
|
|
130
|
-
const xGrowth = (desiredWidth - this.width) / 2;
|
|
131
|
-
const xDest = (window.innerWidth - desiredWidth) / 2;
|
|
132
|
-
this.xTrans = xDest - this.left + xGrowth;
|
|
133
|
-
|
|
134
|
-
const yGrowth = (desiredHeight - this.height) / 2;
|
|
135
|
-
const yDest = (window.innerHeight - desiredHeight) / 2;
|
|
136
|
-
this.yTrans = yDest - this.top + yGrowth;
|
|
137
|
-
|
|
138
|
-
this.scale = desiredScale;
|
|
139
|
-
this.show = true;
|
|
140
110
|
}
|
|
141
111
|
|
|
142
112
|
public handleClick() {
|
|
@@ -144,37 +114,15 @@ export class Lightbox extends RapidElement {
|
|
|
144
114
|
}
|
|
145
115
|
|
|
146
116
|
public render() {
|
|
147
|
-
const styles = {
|
|
148
|
-
transition: `transform ${this.animationTime}ms ease, box-shadow ${this.animationTime}ms ease`
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
if (this.show) {
|
|
152
|
-
styles['left'] = this.left + 'px';
|
|
153
|
-
styles['top'] = this.top + 'px';
|
|
154
|
-
styles['width'] = this.width + 'px';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (this.zoom) {
|
|
158
|
-
styles['transform'] =
|
|
159
|
-
`translate(${this.xTrans}px, ${this.yTrans}px) scale(${this.scale}, ${this.scale})`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
117
|
return html`
|
|
163
118
|
<div
|
|
164
|
-
class=${getClasses({
|
|
165
|
-
|
|
166
|
-
show: this.show,
|
|
167
|
-
zoom: this.zoom
|
|
168
|
-
})}
|
|
119
|
+
class=${getClasses({ backdrop: true, zoom: this.zoom })}
|
|
120
|
+
style="--anim: ${this.animationTime}ms"
|
|
169
121
|
@click=${this.handleClick}
|
|
170
122
|
>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
></div>
|
|
175
|
-
<div class=${getClasses({ matte: true })} style=${styleMap(styles)}>
|
|
176
|
-
${this.show ? html`${this.ele}` : null}
|
|
177
|
-
</div>
|
|
123
|
+
${this.show && this.url
|
|
124
|
+
? html`<img src=${this.url} alt="attachment" />`
|
|
125
|
+
: null}
|
|
178
126
|
</div>
|
|
179
127
|
`;
|
|
180
128
|
}
|
package/src/display/Thumbnail.ts
CHANGED
|
@@ -182,6 +182,11 @@ export class Thumbnail extends RapidElement {
|
|
|
182
182
|
// audio player state
|
|
183
183
|
private audio: HTMLAudioElement | null = null;
|
|
184
184
|
|
|
185
|
+
// set when an image attachment fails to load, so we fall back to an
|
|
186
|
+
// icon instead of the browser's broken-image glyph
|
|
187
|
+
@state()
|
|
188
|
+
private imageError = false;
|
|
189
|
+
|
|
185
190
|
@state()
|
|
186
191
|
private audioPlaying = false;
|
|
187
192
|
|
|
@@ -264,6 +269,8 @@ export class Thumbnail extends RapidElement {
|
|
|
264
269
|
|
|
265
270
|
// convert our attachment to a url and label
|
|
266
271
|
if (changes.has('attachment')) {
|
|
272
|
+
// a new attachment gets a fresh shot at loading its image
|
|
273
|
+
this.imageError = false;
|
|
267
274
|
if (this.attachment) {
|
|
268
275
|
const splitIndex = this.attachment.indexOf(':');
|
|
269
276
|
if (splitIndex === -1) {
|
|
@@ -337,12 +344,27 @@ export class Thumbnail extends RapidElement {
|
|
|
337
344
|
}
|
|
338
345
|
}
|
|
339
346
|
|
|
340
|
-
public handleThumbnailClicked() {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
347
|
+
public handleThumbnailClicked(e?: Event) {
|
|
348
|
+
// a thumbnail click is a self-contained action (lightbox / open / play);
|
|
349
|
+
// don't let it bubble to a clickable ancestor (e.g. a list row that would
|
|
350
|
+
// otherwise navigate away instead of showing the attachment)
|
|
351
|
+
e?.stopPropagation();
|
|
352
|
+
if (
|
|
353
|
+
this.contentType === ThumbnailContentType.IMAGE &&
|
|
354
|
+
this.preview &&
|
|
355
|
+
!this.imageError
|
|
356
|
+
) {
|
|
357
|
+
const lightbox = document.querySelector('temba-lightbox') as Lightbox;
|
|
358
|
+
if (lightbox) {
|
|
359
|
+
window.setTimeout(() => {
|
|
360
|
+
lightbox.showElement(this);
|
|
361
|
+
}, 100);
|
|
362
|
+
} else {
|
|
363
|
+
// no lightbox mounted on this page — just open the image. This must
|
|
364
|
+
// happen synchronously inside the click's user activation or popup
|
|
365
|
+
// blockers will eat it.
|
|
366
|
+
window.open(this.url, '_blank');
|
|
367
|
+
}
|
|
346
368
|
} else if (this.contentType === ThumbnailContentType.LOCATION) {
|
|
347
369
|
// open location in openstreetmap
|
|
348
370
|
const osmUrl = `https://www.openstreetmap.org/?mlat=${this.latitude}&mlon=${this.longitude}#map=15/${this.latitude}/${this.longitude}`;
|
|
@@ -372,12 +394,17 @@ export class Thumbnail extends RapidElement {
|
|
|
372
394
|
: ''}"
|
|
373
395
|
url=${this.url}
|
|
374
396
|
>
|
|
375
|
-
${this.contentType === ThumbnailContentType.IMAGE &&
|
|
397
|
+
${this.contentType === ThumbnailContentType.IMAGE &&
|
|
398
|
+
this.preview &&
|
|
399
|
+
!this.imageError
|
|
376
400
|
? html`<div class=""><div class="download" @click=${this.handleDownload.bind(
|
|
377
401
|
this
|
|
378
402
|
)}><temba-icon size="1" style="color:#fff;" name="download"></temba-icon></div><img
|
|
379
403
|
class="observe thumb ${this.contentType}"
|
|
380
404
|
src="${this.url}"
|
|
405
|
+
@error=${() => {
|
|
406
|
+
this.imageError = true;
|
|
407
|
+
}}
|
|
381
408
|
></img></div>`
|
|
382
409
|
: this.contentType === ThumbnailContentType.AUDIO
|
|
383
410
|
? html`<div class="audio-player">
|
package/src/flow/FlowSearch.ts
CHANGED
|
@@ -315,6 +315,29 @@ function getTypeName(
|
|
|
315
315
|
return 'Unknown';
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Build the display fields for a result that matched on its type name
|
|
320
|
+
* (e.g. "email" matching a "Send Email" action). Shows the first content
|
|
321
|
+
* text as an unhighlighted preview when available, otherwise highlights
|
|
322
|
+
* the match within the type name itself. Returns null if the type name
|
|
323
|
+
* doesn't match the query.
|
|
324
|
+
*/
|
|
325
|
+
function makeTypeNameResult(
|
|
326
|
+
typeName: string,
|
|
327
|
+
query: string,
|
|
328
|
+
contentTexts: string[]
|
|
329
|
+
): Pick<SearchResult, 'fullText' | 'matchStart' | 'matchLength'> | null {
|
|
330
|
+
const idx = typeName.toLowerCase().indexOf(query);
|
|
331
|
+
if (idx === -1) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const preview = contentTexts.find((t) => t.trim());
|
|
335
|
+
if (preview) {
|
|
336
|
+
return { fullText: preview, matchStart: 0, matchLength: 0 };
|
|
337
|
+
}
|
|
338
|
+
return { fullText: typeName, matchStart: idx, matchLength: query.length };
|
|
339
|
+
}
|
|
340
|
+
|
|
318
341
|
/**
|
|
319
342
|
* Get the localized name for a category, falling back to the original name.
|
|
320
343
|
*/
|
|
@@ -640,11 +663,13 @@ export class FlowSearch extends LitElement {
|
|
|
640
663
|
if (node.actions) {
|
|
641
664
|
for (const action of node.actions) {
|
|
642
665
|
const actionConfig = ACTION_CONFIG[action.type];
|
|
666
|
+
const typeName = getTypeName(actionConfig, undefined);
|
|
643
667
|
const searchAction = localizeAction(
|
|
644
668
|
action,
|
|
645
669
|
langLocalization?.[action.uuid]
|
|
646
670
|
);
|
|
647
671
|
const texts = getActionSearchTexts(searchAction);
|
|
672
|
+
let matched = false;
|
|
648
673
|
for (const text of texts) {
|
|
649
674
|
const lowerText = text.toLowerCase();
|
|
650
675
|
const idx = lowerText.indexOf(query);
|
|
@@ -652,15 +677,31 @@ export class FlowSearch extends LitElement {
|
|
|
652
677
|
results.push({
|
|
653
678
|
nodeUuid: node.uuid,
|
|
654
679
|
action,
|
|
655
|
-
typeName
|
|
680
|
+
typeName,
|
|
656
681
|
color: getColor(actionConfig, undefined),
|
|
657
682
|
fullText: text,
|
|
658
683
|
matchStart: idx,
|
|
659
684
|
matchLength: query.length
|
|
660
685
|
});
|
|
686
|
+
matched = true;
|
|
661
687
|
break; // One match per action is enough
|
|
662
688
|
}
|
|
663
689
|
}
|
|
690
|
+
|
|
691
|
+
// Fall back to matching the action type name (e.g. "email"
|
|
692
|
+
// matches all "Send Email" actions)
|
|
693
|
+
if (!matched) {
|
|
694
|
+
const typeMatch = makeTypeNameResult(typeName, query, texts);
|
|
695
|
+
if (typeMatch) {
|
|
696
|
+
results.push({
|
|
697
|
+
nodeUuid: node.uuid,
|
|
698
|
+
action,
|
|
699
|
+
typeName,
|
|
700
|
+
color: getColor(actionConfig, undefined),
|
|
701
|
+
...typeMatch
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
664
705
|
}
|
|
665
706
|
}
|
|
666
707
|
} else {
|
|
@@ -679,6 +720,8 @@ export class FlowSearch extends LitElement {
|
|
|
679
720
|
}
|
|
680
721
|
}
|
|
681
722
|
|
|
723
|
+
const typeName = getTypeName(undefined, nodeConfig);
|
|
724
|
+
let matched = false;
|
|
682
725
|
for (const text of nodeTexts) {
|
|
683
726
|
const lowerText = text.toLowerCase();
|
|
684
727
|
const idx = lowerText.indexOf(query);
|
|
@@ -686,15 +729,31 @@ export class FlowSearch extends LitElement {
|
|
|
686
729
|
results.push({
|
|
687
730
|
nodeUuid: node.uuid,
|
|
688
731
|
action: null,
|
|
689
|
-
typeName
|
|
732
|
+
typeName,
|
|
690
733
|
color: getColor(undefined, nodeConfig),
|
|
691
734
|
fullText: text,
|
|
692
735
|
matchStart: idx,
|
|
693
736
|
matchLength: query.length
|
|
694
737
|
});
|
|
738
|
+
matched = true;
|
|
695
739
|
break; // One match per node is enough
|
|
696
740
|
}
|
|
697
741
|
}
|
|
742
|
+
|
|
743
|
+
// Fall back to matching the node type name (e.g. "wait" matches
|
|
744
|
+
// all "Wait for Response" nodes)
|
|
745
|
+
if (!matched) {
|
|
746
|
+
const typeMatch = makeTypeNameResult(typeName, query, nodeTexts);
|
|
747
|
+
if (typeMatch) {
|
|
748
|
+
results.push({
|
|
749
|
+
nodeUuid: node.uuid,
|
|
750
|
+
action: null,
|
|
751
|
+
typeName,
|
|
752
|
+
color: getColor(undefined, nodeConfig),
|
|
753
|
+
...typeMatch
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
698
757
|
}
|
|
699
758
|
}
|
|
700
759
|
|
|
@@ -838,6 +897,11 @@ export class FlowSearch extends LitElement {
|
|
|
838
897
|
// Replace newlines with spaces for single-line display
|
|
839
898
|
const text = result.fullText.replace(/\n/g, ' ');
|
|
840
899
|
|
|
900
|
+
// Type-name matches show an unhighlighted content preview
|
|
901
|
+
if (matchLength === 0) {
|
|
902
|
+
return html`${text}`;
|
|
903
|
+
}
|
|
904
|
+
|
|
841
905
|
// Show leading context before the match, then the match, then trailing.
|
|
842
906
|
// CSS text-overflow:ellipsis clips the trailing text, so we keep the match
|
|
843
907
|
// near the start. When the match is near the end of the text, show more
|