@sailfish-ai/recorder 1.8.15 → 1.8.18

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.
@@ -0,0 +1,475 @@
1
+ // Helper function to render custom multiselect dropdown
2
+ export function renderCustomMultiSelect(fieldId, fieldName, options, selectedValues, isRequired = false) {
3
+ const requiredMark = isRequired
4
+ ? '<span style="color:#ef4444;">*</span>'
5
+ : "";
6
+ const selectedLabels = options
7
+ .filter((opt) => selectedValues.includes(opt.id || opt.value || opt.name || opt))
8
+ .map((opt) => opt.name || opt.value || opt)
9
+ .join(", ");
10
+ const optionsHTML = options
11
+ .map((opt) => {
12
+ const value = opt.id || opt.value || opt.name || opt;
13
+ const label = opt.name || opt.value || opt;
14
+ const isSelected = selectedValues.includes(value);
15
+ return `
16
+ <div class="sf-multiselect-option" data-value="${value}" data-selected="${isSelected}" style="padding:8px 12px; cursor:pointer; ${isSelected ? "background-color:#e0f2fe;" : ""}" onmouseover="this.style.backgroundColor=this.dataset.selected === 'true' ? '#bae6fd' : '#f1f5f9'" onmouseout="this.style.backgroundColor=this.dataset.selected === 'true' ? '#e0f2fe' : ''" onclick="event.stopPropagation();">
17
+ ${label}
18
+ </div>
19
+ `;
20
+ })
21
+ .join("");
22
+ return `
23
+ <div style="position:relative;">
24
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
25
+ ${fieldName} ${requiredMark}
26
+ </label>
27
+ <div class="sf-custom-multiselect" id="${fieldId}-container" data-field-id="${fieldId}" style="position:relative;">
28
+ <div class="sf-multiselect-input" style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; background-color:white; cursor:pointer; min-height:38px; display:flex; align-items:center;" onclick="document.getElementById('${fieldId}-dropdown').style.display = document.getElementById('${fieldId}-dropdown').style.display === 'none' ? 'block' : 'none';">
29
+ <span style="color:${selectedLabels ? "#000" : "#9ca3af"}; flex:1;">${selectedLabels || "Select..."}</span>
30
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="margin-left:8px;">
31
+ <path d="M4 6L8 10L12 6" stroke="#64748B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
32
+ </svg>
33
+ </div>
34
+ <div id="${fieldId}-dropdown" class="sf-multiselect-dropdown" style="display:none; position:absolute; top:100%; left:0; right:0; margin-top:4px; background:white; border:1px solid #cbd5e1; border-radius:6px; max-height:200px; overflow-y:auto; z-index:1000; box-shadow:0 4px 6px -1px rgba(0,0,0,0.1);">
35
+ ${optionsHTML}
36
+ </div>
37
+ </div>
38
+ </div>
39
+ `;
40
+ }
41
+ // Helper function to generate chevron SVG
42
+ export function getChevronSVG(isExpanded) {
43
+ return isExpanded
44
+ ? `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
45
+ <path d="M12 6L8 10L4 6" stroke="#52525B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
46
+ </svg>`
47
+ : `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
48
+ <path d="M6 12L10 8L6 4" stroke="#52525B" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
49
+ </svg>`;
50
+ }
51
+ // Helper function to generate collapsible section HTML
52
+ export function generateCollapsibleSection(id, title, content, isExpanded = false) {
53
+ return `
54
+ <div class="sf-collapsible-section" style="border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 12px;">
55
+ <button class="sf-collapsible-header" data-target="${id}"
56
+ style="width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 12px; background: #f8fafc; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; color: #0F172A; text-align: left;">
57
+ <span>${title}</span>
58
+ <span class="sf-chevron">${getChevronSVG(isExpanded)}</span>
59
+ </button>
60
+ <div id="${id}" class="sf-collapsible-content" style="display: ${isExpanded ? "block" : "none"}; padding: 16px; border-top: 1px solid #e2e8f0;">
61
+ ${content}
62
+ </div>
63
+ </div>
64
+ `;
65
+ }
66
+ export function renderDynamicField(field, fieldValue, users = []) {
67
+ const fieldId = field.fieldId || field.key;
68
+ const fieldName = field.name;
69
+ const fieldType = field.schema?.type;
70
+ const fieldSystem = field.schema?.system;
71
+ const fieldCustom = field.schema?.custom;
72
+ const isRequired = field.required || false;
73
+ const allowedValues = field.allowedValues;
74
+ // Skip system fields that are handled separately
75
+ const skipFields = [
76
+ "summary",
77
+ "description",
78
+ "project",
79
+ "issuetype",
80
+ "priority",
81
+ ];
82
+ if (skipFields.includes(fieldId) || skipFields.includes(fieldSystem)) {
83
+ return null;
84
+ }
85
+ const requiredMark = isRequired
86
+ ? '<span style="color:#ef4444;">*</span>'
87
+ : "";
88
+ const baseInputStyle = "width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none;";
89
+ switch (fieldType) {
90
+ case "string":
91
+ // Check if it's a textarea (custom field type)
92
+ if (fieldCustom && fieldCustom.includes("textarea")) {
93
+ return `
94
+ <div>
95
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
96
+ ${fieldName} ${requiredMark}
97
+ </label>
98
+ <textarea
99
+ id="${fieldId}"
100
+ class="sf-dynamic-field"
101
+ data-field-id="${fieldId}"
102
+ placeholder="Enter ${fieldName.toLowerCase()}"
103
+ style="${baseInputStyle} height:80px; resize:none;"
104
+ ${isRequired ? "required" : ""}
105
+ >${fieldValue || ""}</textarea>
106
+ </div>
107
+ `;
108
+ }
109
+ return `
110
+ <div>
111
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
112
+ ${fieldName} ${requiredMark}
113
+ </label>
114
+ <input
115
+ type="text"
116
+ id="${fieldId}"
117
+ class="sf-dynamic-field"
118
+ data-field-id="${fieldId}"
119
+ value="${fieldValue || ""}"
120
+ placeholder="Enter ${fieldName.toLowerCase()}"
121
+ style="${baseInputStyle}"
122
+ ${isRequired ? "required" : ""}
123
+ />
124
+ </div>
125
+ `;
126
+ case "number":
127
+ return `
128
+ <div>
129
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
130
+ ${fieldName} ${requiredMark}
131
+ </label>
132
+ <input
133
+ type="number"
134
+ id="${fieldId}"
135
+ class="sf-dynamic-field"
136
+ data-field-id="${fieldId}"
137
+ value="${fieldValue || ""}"
138
+ placeholder="Enter ${fieldName.toLowerCase()}"
139
+ style="${baseInputStyle}"
140
+ step="0.5"
141
+ ${isRequired ? "required" : ""}
142
+ />
143
+ </div>
144
+ `;
145
+ case "date":
146
+ return `
147
+ <div>
148
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
149
+ ${fieldName} ${requiredMark}
150
+ </label>
151
+ <input
152
+ type="date"
153
+ id="${fieldId}"
154
+ class="sf-dynamic-field"
155
+ data-field-id="${fieldId}"
156
+ value="${fieldValue || ""}"
157
+ style="${baseInputStyle}"
158
+ ${isRequired ? "required" : ""}
159
+ />
160
+ </div>
161
+ `;
162
+ case "user":
163
+ if (users && users.length > 0) {
164
+ const options = users
165
+ .map((user) => {
166
+ const label = user.email
167
+ ? `${user.name} (${user.email})`
168
+ : user.name;
169
+ const selected = fieldValue === user.id ? "selected" : "";
170
+ return `<option value="${user.id}" ${selected}>${label}</option>`;
171
+ })
172
+ .join("");
173
+ const hasValue = fieldValue && users.some((u) => u.id === fieldValue);
174
+ return `
175
+ <div>
176
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
177
+ ${fieldName} ${requiredMark}
178
+ </label>
179
+ <select
180
+ id="${fieldId}"
181
+ class="sf-dynamic-field"
182
+ data-field-id="${fieldId}"
183
+ style="${baseInputStyle} appearance:none; cursor:pointer; background-color: white; ${!hasValue ? "color: #9ca3af;" : ""}"
184
+ ${isRequired ? "required" : ""}
185
+ >
186
+ <option value="" disabled ${!hasValue ? "selected" : ""} style="color: #9ca3af;">Select ${fieldName.toLowerCase()}...</option>
187
+ ${options}
188
+ </select>
189
+ </div>
190
+ `;
191
+ }
192
+ return null;
193
+ case "option":
194
+ if (allowedValues && allowedValues.length > 0) {
195
+ const options = allowedValues
196
+ .map((opt) => {
197
+ const value = opt.id || opt.value || opt;
198
+ const label = opt.value || opt.name || opt;
199
+ const selected = fieldValue === value ? "selected" : "";
200
+ return `<option value="${value}" ${selected}>${label}</option>`;
201
+ })
202
+ .join("");
203
+ const hasValue = fieldValue &&
204
+ allowedValues.some((opt) => (opt.id || opt.value || opt) === fieldValue);
205
+ return `
206
+ <div>
207
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
208
+ ${fieldName} ${requiredMark}
209
+ </label>
210
+ <select
211
+ id="${fieldId}"
212
+ class="sf-dynamic-field"
213
+ data-field-id="${fieldId}"
214
+ style="${baseInputStyle} appearance:none; cursor:pointer; background-color: white; ${!hasValue ? "color: #9ca3af;" : ""}"
215
+ ${isRequired ? "required" : ""}
216
+ >
217
+ <option value="" disabled ${!hasValue ? "selected" : ""} style="color: #9ca3af;">Select ${fieldName.toLowerCase()}...</option>
218
+ ${options}
219
+ </select>
220
+ </div>
221
+ `;
222
+ }
223
+ return null;
224
+ case "array":
225
+ // Skip labels as they're handled separately
226
+ if (fieldSystem === "labels") {
227
+ return null;
228
+ }
229
+ // Only render if allowedValues exist and are not empty
230
+ if (allowedValues && allowedValues.length > 0) {
231
+ const fieldValueArray = Array.isArray(fieldValue) ? fieldValue : [];
232
+ return renderCustomMultiSelect(fieldId, fieldName, allowedValues, fieldValueArray, isRequired);
233
+ }
234
+ return null;
235
+ case "parent":
236
+ case "issuelink":
237
+ return `
238
+ <div>
239
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
240
+ ${fieldName} ${requiredMark}
241
+ </label>
242
+ <input
243
+ type="text"
244
+ id="${fieldId}"
245
+ class="sf-dynamic-field"
246
+ data-field-id="${fieldId}"
247
+ value="${fieldValue || ""}"
248
+ placeholder="e.g., PROJ-123"
249
+ style="${baseInputStyle}"
250
+ ${isRequired ? "required" : ""}
251
+ />
252
+ </div>
253
+ `;
254
+ default:
255
+ // For any other field types with set operation, render as text input
256
+ if (field.operations?.includes("set")) {
257
+ return `
258
+ <div>
259
+ <label for="${fieldId}" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
260
+ ${fieldName} ${requiredMark}
261
+ </label>
262
+ <input
263
+ type="text"
264
+ id="${fieldId}"
265
+ class="sf-dynamic-field"
266
+ data-field-id="${fieldId}"
267
+ value="${fieldValue || ""}"
268
+ placeholder="Enter ${fieldName.toLowerCase()}"
269
+ style="${baseInputStyle}"
270
+ ${isRequired ? "required" : ""}
271
+ />
272
+ </div>
273
+ `;
274
+ }
275
+ return null;
276
+ }
277
+ }
278
+ export function generateModalHTML(currentState, initialMode = "lookback") {
279
+ const isStartNow = initialMode === "startnow";
280
+ return `
281
+ <div style="position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:9998;"></div>
282
+ <div style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%);
283
+ background:#fff; padding:24px; border-radius:12px;
284
+ width:476px; max-width:90%; max-height:80vh; overflow-y:auto; z-index:9999;
285
+ box-shadow:0 4px 20px rgba(0,0,0,0.15); font-family:sans-serif;">
286
+
287
+ <button id="sf-modal-close-btn"
288
+ style="position:absolute; top:24px; right:24px; background:none; border:none; cursor:pointer; padding:0;">
289
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
290
+ <path d="M18 6L6 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
291
+ <path d="M6 6L18 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
292
+ </svg>
293
+ </button>
294
+
295
+ <h2 style="font-size:20px; font-weight:600; margin:0 0 20px 0; color:#0F172A;">
296
+ Report an Issue
297
+ </h2>
298
+
299
+ <div style="display:flex; gap:8px; margin-bottom:16px; border:1px solid #cbd5e1; border-radius:8px; padding:4px; background:#f8fafc;">
300
+ <button class="sf-issue-tab" data-mode="lookback"
301
+ style="flex:1; padding:8px; border:none; border-radius:6px; font-size:14px; font-weight:500; cursor:pointer; transition:all 0.2s;
302
+ background:${!isStartNow ? "#fff" : "transparent"}; color:${!isStartNow ? "#0F172A" : "#64748B"};">
303
+ It Already Happened
304
+ </button>
305
+ <button class="sf-issue-tab" data-mode="startnow"
306
+ style="flex:1; padding:8px; border:none; border-radius:6px; font-size:14px; font-weight:500; cursor:pointer; transition:all 0.2s;
307
+ background:${isStartNow ? "#fff" : "transparent"}; color:${isStartNow ? "#0F172A" : "#64748B"};">
308
+ I'll Reproduce It Now
309
+ </button>
310
+ </div>
311
+
312
+ <div id="sf-issue-mode-info" style="display:flex; align-items:center; gap:8px; padding:12px; background:#EFF6FF; border-radius:6px; margin-bottom:20px;">
313
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
314
+ <path d="M8 7V11M8 5H8.005M14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2C11.3137 2 14 4.68629 14 8Z" stroke="#2563EB" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
315
+ </svg>
316
+ <div style="font-size:13px; color:#1E40AF;">
317
+ ${isStartNow
318
+ ? "I want to reproduce the issue right now."
319
+ : "Something already happened. Capture the past few minutes."}
320
+ </div>
321
+ </div>
322
+
323
+ <label for="sf-issue-description" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
324
+ What happened?
325
+ </label>
326
+ <textarea id="sf-issue-description" placeholder="Add description here"
327
+ style="width:100%; height:80px; padding:8px 12px; font-size:14px;
328
+ border:1px solid #cbd5e1; border-radius:6px; margin-bottom:20px;
329
+ resize:none; outline:none;">${currentState.description}</textarea>
330
+
331
+ <!-- Create an Issue Section -->
332
+ <div style="margin-bottom:20px;">
333
+ <label style="display:flex; align-items:center; gap:8px; font-size:14px; font-weight:500; cursor:pointer; margin-bottom:12px;">
334
+ <input type="checkbox" id="sf-create-issue-checkbox" ${currentState.createIssue ? "checked" : ""}
335
+ style="width:16px; height:16px; accent-color:#295DBF; cursor:pointer;">
336
+ Create an Issue
337
+ </label>
338
+
339
+ <div id="sf-issue-fields-container" style="display:${currentState.createIssue ? "block" : "none"};">
340
+ ${generateCollapsibleSection("sf-issue-details", "Issue Details", `
341
+ <div style="display:flex; flex-direction:column; gap:12px;">
342
+ <div>
343
+ <label for="sf-issue-name" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
344
+ Issue name <span style="color:#ef4444;">*</span>
345
+ </label>
346
+ <input type="text" id="sf-issue-name" placeholder="Enter issue name"
347
+ value="${currentState.issueName}"
348
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none;">
349
+ </div>
350
+ <div>
351
+ <label for="sf-issue-desc" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
352
+ Description
353
+ </label>
354
+ <textarea id="sf-issue-desc" placeholder="Enter issue description"
355
+ style="width:100%; height:80px; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; resize:none; outline:none;">${currentState.issueDescription}</textarea>
356
+ </div>
357
+ </div>
358
+ `, currentState.createIssue)}
359
+ </div>
360
+ </div>
361
+
362
+ <!-- Create an Engineering Ticket Section -->
363
+ <div style="margin-bottom:20px;">
364
+ <label style="display:flex; align-items:center; gap:8px; font-size:14px; font-weight:500; cursor:pointer; margin-bottom:12px;">
365
+ <input type="checkbox" id="sf-create-eng-ticket-checkbox" ${currentState.createEngTicket ? "checked" : ""}
366
+ style="width:16px; height:16px; accent-color:#295DBF; cursor:pointer;"
367
+ ${!currentState.createIssue ? "disabled" : ""}>
368
+ Create an Engineering Ticket
369
+ </label>
370
+
371
+ <div id="sf-eng-ticket-fields-container" style="display:${currentState.createEngTicket && currentState.createIssue
372
+ ? "block"
373
+ : "none"};">
374
+ ${generateCollapsibleSection("sf-eng-ticket-details", "Engineering Ticket Details", `
375
+ <div style="display:flex; flex-direction:column; gap:12px;">
376
+ <div>
377
+ <label for="sf-eng-ticket-team" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
378
+ Team
379
+ </label>
380
+ <select id="sf-eng-ticket-team"
381
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none; appearance:none; cursor:pointer; background-color: white;">
382
+ <option value="">Select team...</option>
383
+ </select>
384
+ </div>
385
+ <div>
386
+ <label for="sf-eng-ticket-project" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
387
+ Project
388
+ </label>
389
+ <select id="sf-eng-ticket-project"
390
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none; appearance:none; cursor:pointer; background-color: white;">
391
+ <option value="">Select project...</option>
392
+ </select>
393
+ </div>
394
+ <div>
395
+ <label for="sf-eng-ticket-priority" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
396
+ Priority
397
+ </label>
398
+ <select id="sf-eng-ticket-priority"
399
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none; appearance:none; cursor:pointer; background-color: white;">
400
+ <option value="0">None</option>
401
+ <option value="1">Urgent</option>
402
+ <option value="2">High</option>
403
+ <option value="3">Medium</option>
404
+ <option value="4">Low</option>
405
+ </select>
406
+ </div>
407
+ <div>
408
+ <label for="sf-eng-ticket-labels" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
409
+ Labels
410
+ </label>
411
+ <input type="text" id="sf-eng-ticket-labels" placeholder="Enter labels (comma-separated)"
412
+ value="${currentState.engTicketLabels.join(", ")}"
413
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none;">
414
+ </div>
415
+ <div>
416
+ <label for="sf-eng-ticket-type" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
417
+ Issue Type
418
+ </label>
419
+ <select id="sf-eng-ticket-type"
420
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none; appearance:none; cursor:pointer; background-color: white;">
421
+ <option value="">Select project first...</option>
422
+ </select>
423
+ </div>
424
+ </div>
425
+ `, currentState.createEngTicket && currentState.createIssue)}
426
+ </div>
427
+ </div>
428
+
429
+ <div id="sf-lookback-container" style="display:${isStartNow ? "none" : "block"}; margin-bottom:20px;">
430
+ <label for="sf-lookback-minutes" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
431
+ When did this happen?
432
+ </label>
433
+ <div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
434
+ <div style="position:relative; display:flex; align-items:center; gap:6px;">
435
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
436
+ <circle cx="8" cy="8" r="6" stroke="#64748B" stroke-width="1.5"/>
437
+ <path d="M8 4.5V8H11" stroke="#64748B" stroke-width="1.5" stroke-linecap="round"/>
438
+ </svg>
439
+ <select id="sf-lookback-minutes"
440
+ style="padding:8px 32px 8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; cursor:pointer; outline:none; appearance:none; background:white;">
441
+ <option value="2">2 minutes ago</option>
442
+ <option value="5">5 minutes ago</option>
443
+ <option value="10">10 minutes ago</option>
444
+ </select>
445
+ </div>
446
+ <div style="font-size:13px; color:#64748B;">
447
+ Captures last <span id="sf-minutes-display">2</span> min
448
+ </div>
449
+ </div>
450
+ </div>
451
+
452
+ <div id="sf-record-button-container" style="display:${isStartNow ? "block" : "none"}; margin-bottom:20px;">
453
+ <button id="sf-start-recording-btn"
454
+ style="width:100%; padding:12px; background:#295DBF; color:#fff; border:none; border-radius:6px; font-size:14px; font-weight:500; cursor:pointer;">
455
+ Start Recording
456
+ </button>
457
+ </div>
458
+
459
+ <div id="sf-recording-timer-label" style="display:none; text-align:center; margin-bottom:16px; font-size:13px; color:#64748B;">
460
+ Recorded: <span id="sf-recording-timer-display" style="font-weight:600; color:#0F172A;">00:00</span>
461
+ </div>
462
+
463
+ <div id="sf-modal-footer" style="display:flex; justify-content:flex-end; gap:12px;">
464
+ <button id="sf-issue-cancel-btn"
465
+ style="padding:10px 20px; background:#fff; color:#475569; border:1px solid #cbd5e1; border-radius:6px; font-size:14px; font-weight:500; cursor:pointer;">
466
+ Cancel
467
+ </button>
468
+ <button id="sf-issue-submit-btn"
469
+ style="padding:10px 20px; background:#295DBF; color:#fff; border:none; border-radius:6px; font-size:14px; font-weight:500; cursor:pointer;">
470
+ Submit
471
+ </button>
472
+ </div>
473
+ </div>
474
+ `;
475
+ }
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { readGitSha } from "./env";
7
7
  import { initializeErrorInterceptor } from "./errorInterceptor";
8
8
  import { fetchCaptureSettings, fetchFunctionSpanTrackingEnabled, sendDomainsToNotPropagateHeaderTo, startRecordingSession, } from "./graphql";
9
9
  import { setupIssueReporting } from "./inAppReportIssueModal";
10
+ import { fetchIntegrationData, getIntegrationData, } from "./inAppReportIssueModal/integrations";
10
11
  import { sendMapUuidIfAvailable } from "./mapUuid";
11
12
  import { getUrlAndStoredUuids, initializeConsolePlugin, initializeDomContentEvents, initializeRecording, } from "./recording";
12
13
  import { HAS_DOCUMENT, HAS_LOCAL_STORAGE, HAS_SESSION_STORAGE, HAS_WINDOW, } from "./runtimeEnv";
@@ -819,7 +820,8 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
819
820
  if (isFunctionSpanTrackingEnabled() && (!g.ws || g.ws.readyState !== 1)) {
820
821
  fetchFunctionSpanTrackingEnabled(apiKey, backendApi)
821
822
  .then((funcSpanResponse) => {
822
- const isEnabled = funcSpanResponse.data?.isFunctionSpanTrackingEnabledFromApiKey ?? false;
823
+ const isEnabled = funcSpanResponse.data?.isFunctionSpanTrackingEnabledFromApiKey ??
824
+ false;
823
825
  if (!isEnabled) {
824
826
  // Backend says tracking is NOT active, clear stale localStorage data
825
827
  clearStaleFuncSpanState();
@@ -870,7 +872,8 @@ export async function startRecording({ apiKey, backendApi = "https://api-service
870
872
  const sessionResponse = await startRecordingSession(apiKey, sessionId, backendApi, effectiveServiceIdentifier, effectiveServiceVersion, effectiveMapUuid, effectiveGitSha, effectiveLibrary, metadataWithAppUrl);
871
873
  if (sessionResponse.data?.startRecordingSession) {
872
874
  // Extract env value from serviceAdditionalMetadata
873
- const envValue = serviceAdditionalMetadata?.env || serviceAdditionalMetadata?.environment;
875
+ const envValue = serviceAdditionalMetadata?.env ||
876
+ serviceAdditionalMetadata?.environment;
874
877
  const websocket = await initializeRecording(captureSettings, backendApi, apiKey, sessionId, envValue);
875
878
  g.ws = websocket;
876
879
  g.initialized = true;
@@ -917,12 +920,24 @@ export const initRecorder = async (options) => {
917
920
  await startRecording(options);
918
921
  // Set up the issue reporting UI once
919
922
  if (!g.issueReportingInit) {
923
+ const backendApiUrl = options.backendApi ?? "https://api-service.sailfishqa.com";
924
+ // Fetch integration data before setting up issue reporting
925
+ let integrationData = null;
926
+ try {
927
+ await fetchIntegrationData(options.apiKey, backendApiUrl);
928
+ integrationData = getIntegrationData();
929
+ }
930
+ catch (error) {
931
+ console.warn("[Sailfish] Failed to fetch integration data for issue reporting:", error);
932
+ // Continue with null integration data - the modal will work without it
933
+ }
920
934
  setupIssueReporting({
921
935
  apiKey: options.apiKey,
922
- backendApi: options.backendApi ?? "https://api-service.sailfishqa.com",
936
+ backendApi: backendApiUrl,
923
937
  getSessionId: () => getOrSetSessionId(),
924
938
  shortcuts: options.reportIssueShortcuts,
925
939
  customBaseUrl: options.customBaseUrl,
940
+ integrationData,
926
941
  });
927
942
  g.issueReportingInit = true;
928
943
  }