@sailfish-ai/recorder 1.8.13 → 1.8.17
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/graphql.js +80 -0
- package/dist/{inAppReportIssueModal.js → inAppReportIssueModal/index.js} +660 -83
- package/dist/inAppReportIssueModal/integrations.js +240 -0
- package/dist/inAppReportIssueModal/state.js +69 -0
- package/dist/inAppReportIssueModal/types.js +16 -0
- package/dist/inAppReportIssueModal/ui.js +475 -0
- package/dist/index.js +18 -3
- package/dist/recorder.cjs +1952 -1407
- package/dist/recorder.js +1632 -1085
- package/dist/recorder.js.br +0 -0
- package/dist/recorder.js.gz +0 -0
- package/dist/recorder.umd.cjs +1630 -1085
- package/dist/types/graphql.d.ts +3 -1
- package/dist/types/{inAppReportIssueModal.d.ts → inAppReportIssueModal/index.d.ts} +2 -0
- package/dist/types/inAppReportIssueModal/integrations.d.ts +12 -0
- package/dist/types/inAppReportIssueModal/state.d.ts +18 -0
- package/dist/types/inAppReportIssueModal/types.d.ts +40 -0
- package/dist/types/inAppReportIssueModal/ui.d.ts +6 -0
- package/dist/types/types.d.ts +30 -0
- package/dist/websocket.js +9 -9
- package/package.json +7 -2
|
@@ -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 ??
|
|
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 ||
|
|
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:
|
|
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
|
}
|