@sailfish-ai/recorder 1.8.15 → 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.
@@ -1,4 +1,8 @@
1
- import { createTriageFromRecorder } from "./graphql";
1
+ import { createTriageAndIssueFromRecorder, createTriageFromRecorder, } from "../graphql";
2
+ import { getFieldsForProject, getIntegrationData, getProjectsForTeam, getUsers, hasValidIntegration, updateFormWithIntegrationData, updateIssueTypeOptions, } from "./integrations";
3
+ import { currentState, isRecording, recordingEndTime, recordingStartTime, resetState, setIsRecording, setRecordingEndTime, setRecordingStartTime, setTimerInterval, timerInterval, } from "./state";
4
+ import { STORAGE_KEYS } from "./types";
5
+ import { getChevronSVG, renderCustomMultiSelect, renderDynamicField, } from "./ui";
2
6
  // TODO - enable configuration by keyboard typing in UI
3
7
  const DEFAULT_SHORTCUTS = {
4
8
  enabled: false,
@@ -16,17 +20,167 @@ export const ReportIssueContext = {
16
20
  backendApi: null,
17
21
  triageBaseUrl: "https://app.sailfishqa.com",
18
22
  deactivateIsolation: () => { },
23
+ integrationData: null,
19
24
  };
20
25
  let modalEl = null;
21
- let currentState = {
22
- mode: "lookback",
23
- description: "",
24
- occurredInThisTab: true,
25
- };
26
- let recordingStartTime = null;
27
- let recordingEndTime = null;
28
- let timerInterval = null;
29
- let isRecording = false;
26
+ // Function to set up custom multiselect listeners
27
+ function setupCustomMultiSelectListeners(fieldId, onChange) {
28
+ const container = document.getElementById(`${fieldId}-container`);
29
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
30
+ if (!container || !dropdown)
31
+ return;
32
+ // Handle option clicks
33
+ const options = dropdown.querySelectorAll(".sf-multiselect-option");
34
+ options.forEach((option) => {
35
+ option.addEventListener("click", (e) => {
36
+ e.stopPropagation();
37
+ const optionEl = option;
38
+ // Toggle selection
39
+ const isSelected = optionEl.dataset.selected === "true";
40
+ optionEl.dataset.selected = String(!isSelected);
41
+ // Update background color
42
+ if (!isSelected) {
43
+ optionEl.style.backgroundColor = "#e0f2fe";
44
+ }
45
+ else {
46
+ optionEl.style.backgroundColor = "";
47
+ }
48
+ // Get all selected values and labels
49
+ const selectedValues = [];
50
+ const selectedLabels = [];
51
+ dropdown.querySelectorAll(".sf-multiselect-option").forEach((opt) => {
52
+ const optEl = opt;
53
+ if (optEl.dataset.selected === "true") {
54
+ selectedValues.push(optEl.dataset.value || "");
55
+ selectedLabels.push(optEl.textContent || "");
56
+ }
57
+ });
58
+ // Update input display
59
+ const input = container.querySelector(".sf-multiselect-input span");
60
+ const displayText = selectedLabels.join(", ");
61
+ if (input) {
62
+ input.textContent = displayText || "Select...";
63
+ input.style.color = displayText ? "#000" : "#9ca3af";
64
+ }
65
+ // Call onChange callback
66
+ onChange(selectedValues);
67
+ });
68
+ });
69
+ // Close dropdown on outside click
70
+ document.addEventListener("click", (e) => {
71
+ const target = e.target;
72
+ if (!container.contains(target)) {
73
+ dropdown.style.display = "none";
74
+ }
75
+ });
76
+ }
77
+ // Function to render all dynamic fields for a project and issue type
78
+ function renderDynamicFields(projectId, issueTypeId) {
79
+ const dynamicFieldsContainer = document.getElementById("sf-dynamic-fields-container");
80
+ if (!dynamicFieldsContainer)
81
+ return;
82
+ if (!projectId) {
83
+ dynamicFieldsContainer.innerHTML =
84
+ '<div style="font-size:14px; color:#64748B;">Select a project to see additional fields</div>';
85
+ return;
86
+ }
87
+ const fields = getFieldsForProject(projectId, issueTypeId);
88
+ const users = getUsers();
89
+ if (!fields || fields.length === 0) {
90
+ dynamicFieldsContainer.innerHTML = "";
91
+ return;
92
+ }
93
+ const renderedFields = fields
94
+ .map((field) => renderDynamicField(field, currentState.engTicketCustomFields[field.fieldId || field.key], users))
95
+ .filter(Boolean)
96
+ .join("");
97
+ dynamicFieldsContainer.innerHTML = renderedFields || "";
98
+ // Set up listeners for custom multiselects in dynamic fields
99
+ fields.forEach((field) => {
100
+ const fieldId = field.fieldId || field.key;
101
+ const fieldType = field.schema?.type;
102
+ const allowedValues = field.allowedValues;
103
+ if (fieldType === "array" && allowedValues && allowedValues.length > 0) {
104
+ setupCustomMultiSelectListeners(fieldId, (selectedValues) => {
105
+ currentState.engTicketCustomFields[fieldId] = selectedValues;
106
+ });
107
+ }
108
+ });
109
+ }
110
+ // Function to generate engineering ticket fields HTML based on integration data
111
+ function generateEngTicketFieldsHTML() {
112
+ const integrationData = ReportIssueContext.integrationData;
113
+ if (!integrationData)
114
+ return "";
115
+ const isJira = integrationData.provider?.toLowerCase() === "jira";
116
+ const hasTeams = integrationData.teams &&
117
+ Array.isArray(integrationData.teams) &&
118
+ integrationData.teams.length > 0;
119
+ let fieldsHTML = "<div style='display:flex; flex-direction:column; gap:12px;'>";
120
+ // Team field (only for Linear - when teams exist)
121
+ if (hasTeams) {
122
+ fieldsHTML += `
123
+ <div>
124
+ <label for="sf-eng-ticket-team" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
125
+ Team
126
+ </label>
127
+ <select id="sf-eng-ticket-team"
128
+ 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; color: #9ca3af;">
129
+ <option value="" disabled selected style="color: #9ca3af;">Select team...</option>
130
+ </select>
131
+ </div>
132
+ `;
133
+ }
134
+ // Project field (always shown)
135
+ fieldsHTML += `
136
+ <div>
137
+ <label for="sf-eng-ticket-project" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
138
+ Project
139
+ </label>
140
+ <select id="sf-eng-ticket-project"
141
+ 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; color: #9ca3af;">
142
+ <option value="" disabled selected style="color: #9ca3af;">Select project...</option>
143
+ </select>
144
+ </div>
145
+ `;
146
+ // Issue Type field (only for Jira)
147
+ if (isJira) {
148
+ fieldsHTML += `
149
+ <div>
150
+ <label for="sf-eng-ticket-type" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
151
+ Issue Type
152
+ </label>
153
+ <select id="sf-eng-ticket-type"
154
+ 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; color: #9ca3af;">
155
+ <option value="" disabled selected style="color: #9ca3af;">Select project first...</option>
156
+ </select>
157
+ </div>
158
+ `;
159
+ }
160
+ // Priority field (always shown)
161
+ fieldsHTML += `
162
+ <div>
163
+ <label for="sf-eng-ticket-priority" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
164
+ Priority
165
+ </label>
166
+ <select id="sf-eng-ticket-priority"
167
+ 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;">
168
+ </select>
169
+ </div>
170
+ `;
171
+ // Labels field (only if integration has labels)
172
+ if (integrationData.labels &&
173
+ Array.isArray(integrationData.labels) &&
174
+ integrationData.labels.length > 0) {
175
+ fieldsHTML += renderCustomMultiSelect("sf-eng-ticket-labels", "Labels", integrationData.labels, currentState.engTicketLabels, false);
176
+ }
177
+ // Dynamic Fields Container
178
+ fieldsHTML += `
179
+ <div id="sf-dynamic-fields-container" style="display: flex; flex-direction: column; gap: 12px;"></div>
180
+ `;
181
+ fieldsHTML += "</div>";
182
+ return fieldsHTML;
183
+ }
30
184
  function isMacPlatform() {
31
185
  // Newer API (Chrome, Edge, Opera)
32
186
  const uaData = navigator.userAgentData;
@@ -95,6 +249,7 @@ export function setupIssueReporting(options) {
95
249
  ReportIssueContext.apiKey = options.apiKey;
96
250
  ReportIssueContext.backendApi = options.backendApi;
97
251
  ReportIssueContext.resolveSessionId = options.getSessionId;
252
+ ReportIssueContext.integrationData = options.integrationData || null;
98
253
  if (options.customBaseUrl)
99
254
  ReportIssueContext.triageBaseUrl = options.customBaseUrl;
100
255
  ReportIssueContext.shortcuts = mergeShortcutsConfig(options.shortcuts);
@@ -188,16 +343,12 @@ function closeModal() {
188
343
  }
189
344
  modalEl = null;
190
345
  if (!isRecording) {
191
- currentState = {
192
- mode: "lookback",
193
- description: "",
194
- occurredInThisTab: true,
195
- };
196
- recordingStartTime = null;
197
- recordingEndTime = null;
346
+ resetState();
198
347
  }
199
- if (timerInterval)
348
+ if (timerInterval) {
200
349
  clearInterval(timerInterval);
350
+ setTimerInterval(null);
351
+ }
201
352
  }
202
353
  function activateModalIsolation(modal) {
203
354
  // A. Ensure modal is a proper dialog & focus anchor
@@ -378,31 +529,38 @@ function injectModalHTML(initialMode = "lookback") {
378
529
  modalEl.innerHTML = `
379
530
  <div style="position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:9998;"></div>
380
531
  <div style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%);
381
- background:#fff; padding:24px; border-radius:12px;
382
- width:476px; max-width:90%; z-index:9999;
383
- box-shadow:0 4px 20px rgba(0,0,0,0.15); font-family:sans-serif;">
532
+ background:#fff; border-radius:12px;
533
+ width:476px; max-width:90%; max-height:90vh; z-index:9999;
534
+ box-shadow:0 4px 20px rgba(0,0,0,0.15); font-family:sans-serif;
535
+ display:flex; flex-direction:column;">
384
536
 
385
- <button id="sf-modal-close-btn"
386
- style="position:absolute; top:24px; right:24px;">
387
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
388
- <path d="M18 6L6 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
389
- <path d="M6 6L18 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
390
- </svg>
391
- </button>
537
+ <!-- Fixed Header -->
538
+ <div style="padding:24px 24px 16px 24px; flex-shrink:0; position:relative;">
539
+ <button id="sf-modal-close-btn"
540
+ style="position:absolute; top:24px; right:24px; background:none; border:none; cursor:pointer; padding:0;">
541
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
542
+ <path d="M18 6L6 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
543
+ <path d="M6 6L18 18" stroke="#71717A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
544
+ </svg>
545
+ </button>
392
546
 
393
- <h2 style="font-size:18px; font-weight:600; margin-bottom:16px;">Report Issue</h2>
547
+ <h2 style="font-size:18px; font-weight:600; margin-bottom:16px;">Report Issue</h2>
394
548
 
395
- <div id="sf-issue-tabs" style="display:flex; gap:4px; margin-bottom:16px; background:#f1f5f9; padding:6px; border-radius:6px; width: fit-content;">
396
- <button id="sf-tab-lookback" data-mode="lookback" class="sf-issue-tab ${!isStartNow ? "active" : ""}"
397
- style="padding:6px 12px; border:none; background:${!isStartNow ? "white" : "transparent"}; color: ${!isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
398
- Existing ${getShortcutLabelFromContext("openModalExistingMode")}
399
- </button>
400
- <button id="sf-tab-startnow" data-mode="startnow" class="sf-issue-tab ${isStartNow ? "active" : ""}"
401
- style="padding:6px 12px; border:none; background:${isStartNow ? "white" : "transparent"}; color: ${isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
402
- Capture new ${getShortcutLabelFromContext("openModalCaptureNewMode")}
403
- </button>
549
+ <div id="sf-issue-tabs" style="display:flex; gap:4px; background:#f1f5f9; padding:6px; border-radius:6px; width: fit-content;">
550
+ <button id="sf-tab-lookback" data-mode="lookback" class="sf-issue-tab ${!isStartNow ? "active" : ""}"
551
+ style="padding:6px 12px; border:none; background:${!isStartNow ? "white" : "transparent"}; color: ${!isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
552
+ Existing ${getShortcutLabelFromContext("openModalExistingMode")}
553
+ </button>
554
+ <button id="sf-tab-startnow" data-mode="startnow" class="sf-issue-tab ${isStartNow ? "active" : ""}"
555
+ style="padding:6px 12px; border:none; background:${isStartNow ? "white" : "transparent"}; color: ${isStartNow ? "#0F172A" : "#64748B"}; border-radius:4px; font-size:14px; cursor:pointer; font-weight:500; height:32px; display: flex; align-items: center; gap: 8px;">
556
+ Capture new ${getShortcutLabelFromContext("openModalCaptureNewMode")}
557
+ </button>
558
+ </div>
404
559
  </div>
405
560
 
561
+ <!-- Scrollable Content -->
562
+ <div style="flex:1; overflow-y:auto; padding:0 24px;">
563
+
406
564
  <div id="sf-issue-mode-info" style="display:flex; align-items:flex-start; gap:8px; margin-bottom:24px;">
407
565
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-top:4px;">
408
566
  <g clip-path="url(#clip0_2477_11797)">
@@ -427,6 +585,7 @@ function injectModalHTML(initialMode = "lookback") {
427
585
  border:1px solid #cbd5e1; border-radius:6px; margin-bottom:20px;
428
586
  resize:none; outline:none;">${currentState.description}</textarea>
429
587
 
588
+ <!-- When did this happen Section -->
430
589
  <div id="sf-lookback-container" style="display:${isStartNow ? "none" : "block"}; margin-bottom:20px;">
431
590
  <label for="sf-lookback-minutes" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
432
591
  When did this happen?
@@ -483,35 +642,82 @@ function injectModalHTML(initialMode = "lookback") {
483
642
  </div>
484
643
  </div>
485
644
 
486
- <div id="sf-modal-footer" style="display:flex; justify-content:${isStartNow ? "space-between" : "flex-end"}; align-items:flex-end;">
645
+ <!-- Divider -->
646
+ <div style="height: 1px; background: #e2e8f0; margin: 20px 0;"></div>
487
647
 
488
- <div id="sf-record-button-container" style="display:${isStartNow ? "block" : "none"};">
489
- <div id="sf-recording-timer-label" style="display:none; font-size:14px; margin-bottom:20px;">
490
- Recording: <span id="sf-recording-timer-display">00:00</span>
491
- </div>
492
- <button id="sf-start-recording-btn"
493
- style="display:flex; align-items:center; gap:8px; border:1px solid #fc5555;
494
- background:transparent; padding:6px 12px; font-size:14px;
495
- color: #fc5555; border-radius:6px; cursor:pointer; font-weight:500;">
496
- <div id="sf-record-icon" style="padding: 6px; border-radius: 6px; border: 1px solid #fc5555; cursor: pointer;">
497
- <div style="width: 14px; height: 14px; background: #fc5555; border-radius: 50%; border: 1px solid #991b1b;"></div>
648
+ <!-- Create an Issue & Engineering Ticket Section -->
649
+ <div style="margin-bottom:20px;">
650
+ <!-- Checkboxes on same line -->
651
+ <div style="display:flex; align-items:center; gap:24px; margin-bottom:16px;">
652
+ <label style="display:flex; align-items:center; gap:8px; font-size:14px; font-weight:500; cursor:pointer;">
653
+ <input type="checkbox" id="sf-create-issue-checkbox" ${currentState.createIssue ? "checked" : ""}
654
+ style="width:16px; height:16px; accent-color:#295DBF; cursor:pointer;">
655
+ Create an Issue
656
+ </label>
657
+
658
+ <label id="sf-create-eng-ticket-label" style="display:${ReportIssueContext.integrationData ? "flex" : "none"}; align-items:center; gap:8px; font-size:14px; font-weight:500; cursor:pointer;">
659
+ <input type="checkbox" id="sf-create-eng-ticket-checkbox" ${currentState.createEngTicket ? "checked" : ""}
660
+ style="width:16px; height:16px; accent-color:#295DBF; cursor:pointer;">
661
+ Create an Eng Ticket
662
+ </label>
663
+ </div>
664
+
665
+ <!-- Issue Title Field (always shown when create issue is checked) -->
666
+ <div id="sf-issue-fields-container" style="display:${currentState.createIssue ? "block" : "none"};">
667
+ <div style="display:flex; flex-direction:column; gap:12px;">
668
+ <div>
669
+ <label for="sf-issue-name" style="display:block; font-size:14px; font-weight:500; margin-bottom:6px;">
670
+ Title <span style="color:#ef4444;">*</span>
671
+ </label>
672
+ <input type="text" id="sf-issue-name" placeholder="Enter title"
673
+ value="${currentState.issueName}"
674
+ style="width:100%; padding:8px 12px; font-size:14px; border:1px solid #cbd5e1; border-radius:6px; outline:none;">
498
675
  </div>
499
- <span>Start Recording</span>
500
- ${getShortcutLabelFromContext("startRecording")}
501
- </button>
676
+ </div>
502
677
  </div>
503
678
 
504
- <button id="sf-issue-submit-btn"
505
- style="background: #295DBF; color:white; border:none; padding:8px 16px;
506
- border-radius:6px; font-size:14px; line-height: 24px; font-weight:500;
507
- cursor:${isStartNow ? "not-allowed" : "pointer"}; opacity:${isStartNow ? "0.4" : "1"}; margin-bottom:1px" ${isStartNow ? "disabled" : ""}>
508
- Report <span style="margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: #94A3B8; font-size: 12px; line-height:16px;">
509
- ${getShortcutLabelFromContext("submitReport")}
510
- </span>
511
- </button>
679
+ <!-- Engineering Ticket Fields (shown when create eng ticket is checked) -->
680
+ <div id="sf-eng-ticket-fields-container" style="display:${currentState.createEngTicket ? "block" : "none"}; margin-top: ${currentState.createIssue ? "12px" : "0"};">
681
+ ${generateEngTicketFieldsHTML()}
682
+ </div>
512
683
  </div>
513
- <div style="display:flex; justify-content:center; font-size: 12px; margin-top: 12px; color: #295dbf;">
514
- <a href="mailto:info@sailfishqa.com?subject=I'd%20love%20to%20learn%20more&body=Hey%2C%20Sailfish%20AI%20team%20-%20I'd%20love%20to%20learn%20more%20about%20Sailfish%20AI!">Powered by Sailfish AI</a>
684
+
685
+ <!-- Divider -->
686
+ <div style="height: 1px; background: #e2e8f0; margin-top: 20px;"></div>
687
+ </div>
688
+
689
+ <!-- Fixed Footer -->
690
+ <div style="padding:20px 24px; flex-shrink:0;">
691
+ <div id="sf-modal-footer" style="display:flex; justify-content:${isStartNow ? "space-between" : "flex-end"}; align-items:flex-end;">
692
+
693
+ <div id="sf-record-button-container" style="display:${isStartNow ? "block" : "none"};">
694
+ <div id="sf-recording-timer-label" style="display:none; font-size:14px; margin-bottom:20px;">
695
+ Recording: <span id="sf-recording-timer-display">00:00</span>
696
+ </div>
697
+ <button id="sf-start-recording-btn"
698
+ style="display:flex; align-items:center; gap:8px; border:1px solid #fc5555;
699
+ background:transparent; padding:6px 12px; font-size:14px;
700
+ color: #fc5555; border-radius:6px; cursor:pointer; font-weight:500;">
701
+ <div id="sf-record-icon" style="padding: 6px; border-radius: 6px; border: 1px solid #fc5555; cursor: pointer;">
702
+ <div style="width: 14px; height: 14px; background: #fc5555; border-radius: 50%; border: 1px solid #991b1b;"></div>
703
+ </div>
704
+ <span>Start Recording</span>
705
+ ${getShortcutLabelFromContext("startRecording")}
706
+ </button>
707
+ </div>
708
+
709
+ <button id="sf-issue-submit-btn"
710
+ style="background: #295DBF; color:white; border:none; padding:8px 16px;
711
+ border-radius:6px; font-size:14px; line-height: 24px; font-weight:500;
712
+ cursor:${isStartNow ? "not-allowed" : "pointer"}; opacity:${isStartNow ? "0.4" : "1"}; margin-bottom:1px" ${isStartNow ? "disabled" : ""}>
713
+ Report <span style="margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: #94A3B8; font-size: 12px; line-height:16px;">
714
+ ${getShortcutLabelFromContext("submitReport")}
715
+ </span>
716
+ </button>
717
+ </div>
718
+ <div style="display:flex; justify-content:center; font-size: 12px; margin-top: 12px; color: #295dbf;">
719
+ <a href="mailto:info@sailfishqa.com?subject=I'd%20love%20to%20learn%20more&body=Hey%2C%20Sailfish%20AI%20team%20-%20I'd%20love%20to%20learn%20more%20about%20Sailfish%20AI!">Powered by Sailfish AI</a>
720
+ </div>
515
721
  </div>
516
722
  </div>
517
723
  `;
@@ -519,6 +725,47 @@ function injectModalHTML(initialMode = "lookback") {
519
725
  document.body.appendChild(modalEl);
520
726
  bindListeners();
521
727
  ReportIssueContext.deactivateIsolation = activateModalIsolation(modalEl);
728
+ // If engineering ticket is already enabled on load, initialize the form
729
+ if (ReportIssueContext.integrationData && currentState.createEngTicket) {
730
+ initializeEngTicketForm();
731
+ }
732
+ else if (!ReportIssueContext.integrationData) {
733
+ // No integration, disable eng ticket
734
+ currentState.createEngTicket = false;
735
+ }
736
+ }
737
+ function initializeEngTicketForm() {
738
+ const integrationData = ReportIssueContext.integrationData;
739
+ if (!integrationData) {
740
+ return;
741
+ }
742
+ // Set default values from integration data
743
+ if (!currentState.engTicketTeam && integrationData.defaultTeam) {
744
+ currentState.engTicketTeam = integrationData.defaultTeam;
745
+ }
746
+ if (!currentState.engTicketProject && integrationData.defaultProject) {
747
+ currentState.engTicketProject = integrationData.defaultProject;
748
+ }
749
+ if (!currentState.engTicketPriority && integrationData.defaultPriority) {
750
+ currentState.engTicketPriority = integrationData.defaultPriority;
751
+ }
752
+ // Update form with integration data
753
+ updateFormWithIntegrationData(currentState);
754
+ // Handle reporter field for Jira
755
+ if (integrationData.provider?.toLowerCase() === "jira" &&
756
+ integrationData.jiraReporterAccountId &&
757
+ currentState.engTicketProject) {
758
+ const fields = getFieldsForProject(currentState.engTicketProject, currentState.engTicketIssueType);
759
+ const reporterField = fields.find((f) => f.fieldId === "reporter");
760
+ if (reporterField && !currentState.engTicketCustomFields["reporter"]) {
761
+ currentState.engTicketCustomFields["reporter"] =
762
+ integrationData.jiraReporterAccountId;
763
+ }
764
+ }
765
+ // If a project is already selected, render dynamic fields
766
+ if (currentState.engTicketProject) {
767
+ renderDynamicFields(currentState.engTicketProject, currentState.engTicketIssueType);
768
+ }
522
769
  }
523
770
  function setActiveTab(mode) {
524
771
  currentState.mode = mode;
@@ -617,6 +864,255 @@ function bindListeners() {
617
864
  }
618
865
  });
619
866
  }
867
+ // Collapsible sections toggle
868
+ const collapsibleHeaders = modalEl?.querySelectorAll(".sf-collapsible-header");
869
+ collapsibleHeaders?.forEach((header) => {
870
+ header.addEventListener("click", (e) => {
871
+ const button = e.currentTarget;
872
+ const targetId = button.dataset.target;
873
+ const content = document.getElementById(targetId);
874
+ const chevron = button.querySelector(".sf-chevron");
875
+ if (content && chevron) {
876
+ const isExpanded = content.style.display !== "none";
877
+ content.style.display = isExpanded ? "none" : "block";
878
+ chevron.innerHTML = getChevronSVG(!isExpanded);
879
+ }
880
+ });
881
+ });
882
+ // Create Issue checkbox
883
+ const createIssueCheckbox = document.getElementById("sf-create-issue-checkbox");
884
+ const issueFieldsContainer = document.getElementById("sf-issue-fields-container");
885
+ const createEngTicketCheckbox = document.getElementById("sf-create-eng-ticket-checkbox");
886
+ const engTicketFieldsContainer = document.getElementById("sf-eng-ticket-fields-container");
887
+ if (createIssueCheckbox) {
888
+ createIssueCheckbox.addEventListener("change", () => {
889
+ const isChecked = createIssueCheckbox.checked;
890
+ currentState.createIssue = isChecked;
891
+ localStorage.setItem(STORAGE_KEYS.CREATE_ISSUE, String(isChecked));
892
+ if (issueFieldsContainer) {
893
+ issueFieldsContainer.style.display = isChecked ? "block" : "none";
894
+ }
895
+ // If unchecking create issue, also uncheck eng ticket
896
+ if (!isChecked && createEngTicketCheckbox) {
897
+ createEngTicketCheckbox.checked = false;
898
+ currentState.createEngTicket = false;
899
+ localStorage.setItem(STORAGE_KEYS.CREATE_ENG_TICKET, "false");
900
+ if (engTicketFieldsContainer) {
901
+ engTicketFieldsContainer.style.display = "none";
902
+ }
903
+ }
904
+ });
905
+ }
906
+ // Create Engineering Ticket checkbox
907
+ if (createEngTicketCheckbox) {
908
+ createEngTicketCheckbox.addEventListener("change", async () => {
909
+ const isChecked = createEngTicketCheckbox.checked;
910
+ currentState.createEngTicket = isChecked;
911
+ localStorage.setItem(STORAGE_KEYS.CREATE_ENG_TICKET, String(isChecked));
912
+ // If checking eng ticket, also check create issue
913
+ if (isChecked && !currentState.createIssue) {
914
+ currentState.createIssue = true;
915
+ localStorage.setItem(STORAGE_KEYS.CREATE_ISSUE, "true");
916
+ if (createIssueCheckbox) {
917
+ createIssueCheckbox.checked = true;
918
+ }
919
+ if (issueFieldsContainer) {
920
+ issueFieldsContainer.style.display = "block";
921
+ }
922
+ }
923
+ if (engTicketFieldsContainer) {
924
+ engTicketFieldsContainer.style.display = isChecked ? "block" : "none";
925
+ }
926
+ // Validate integration data when checkbox is checked
927
+ // (Data should already be pre-loaded in initRecorder)
928
+ if (isChecked) {
929
+ // Check if we have a valid integration
930
+ if (!hasValidIntegration()) {
931
+ // Uncheck the checkbox if no valid integration
932
+ createEngTicketCheckbox.checked = false;
933
+ currentState.createEngTicket = false;
934
+ localStorage.setItem(STORAGE_KEYS.CREATE_ENG_TICKET, "false");
935
+ // Hide fields
936
+ if (engTicketFieldsContainer) {
937
+ engTicketFieldsContainer.style.display = "none";
938
+ }
939
+ alert("No engineering ticket integration found. Please install and configure an integration (Jira, Linear, or Zendesk) first.");
940
+ return;
941
+ }
942
+ // Update form fields with integration data if available
943
+ const integrationData = getIntegrationData();
944
+ if (integrationData) {
945
+ // Set default values
946
+ if (!currentState.engTicketTeam && integrationData.defaultTeam) {
947
+ currentState.engTicketTeam = integrationData.defaultTeam;
948
+ }
949
+ if (!currentState.engTicketProject &&
950
+ integrationData.defaultProject) {
951
+ currentState.engTicketProject = integrationData.defaultProject;
952
+ }
953
+ if (!currentState.engTicketPriority &&
954
+ integrationData.defaultPriority) {
955
+ currentState.engTicketPriority = integrationData.defaultPriority;
956
+ }
957
+ updateFormWithIntegrationData(currentState);
958
+ // Handle reporter field for Jira
959
+ if (integrationData.provider?.toLowerCase() === "jira" &&
960
+ integrationData.jiraReporterAccountId &&
961
+ currentState.engTicketProject) {
962
+ const fields = getFieldsForProject(currentState.engTicketProject, currentState.engTicketIssueType);
963
+ const reporterField = fields.find((f) => f.fieldId === "reporter");
964
+ if (reporterField &&
965
+ !currentState.engTicketCustomFields["reporter"]) {
966
+ currentState.engTicketCustomFields["reporter"] =
967
+ integrationData.jiraReporterAccountId;
968
+ }
969
+ }
970
+ // Render dynamic fields if a project is already selected
971
+ const projectSelect = document.getElementById("sf-eng-ticket-project");
972
+ const issueTypeSelect = document.getElementById("sf-eng-ticket-type");
973
+ if (projectSelect && projectSelect.value) {
974
+ renderDynamicFields(projectSelect.value, issueTypeSelect?.value);
975
+ }
976
+ }
977
+ }
978
+ });
979
+ }
980
+ // Issue form fields
981
+ const issueNameInput = document.getElementById("sf-issue-name");
982
+ if (issueNameInput) {
983
+ issueNameInput.addEventListener("input", () => {
984
+ currentState.issueName = issueNameInput.value;
985
+ });
986
+ }
987
+ // Engineering ticket form fields
988
+ const engTeamSelect = document.getElementById("sf-eng-ticket-team");
989
+ const engProjectSelect = document.getElementById("sf-eng-ticket-project");
990
+ const engPrioritySelect = document.getElementById("sf-eng-ticket-priority");
991
+ const engLabelsContainer = document.getElementById("sf-eng-ticket-labels-container");
992
+ const engTypeSelect = document.getElementById("sf-eng-ticket-type");
993
+ if (engTeamSelect) {
994
+ engTeamSelect.addEventListener("change", () => {
995
+ currentState.engTicketTeam = engTeamSelect.value;
996
+ // Update text color based on selection
997
+ engTeamSelect.style.color = engTeamSelect.value ? "" : "#9ca3af";
998
+ // Update projects dropdown when team changes (for Linear)
999
+ if (engProjectSelect) {
1000
+ const projects = getProjectsForTeam(engTeamSelect.value);
1001
+ const projectSelect = document.getElementById("sf-eng-ticket-project");
1002
+ if (projectSelect) {
1003
+ // Clear current selection
1004
+ currentState.engTicketProject = "";
1005
+ currentState.engTicketCustomFields = {};
1006
+ // Populate new projects
1007
+ projectSelect.innerHTML =
1008
+ '<option value="">Select project...</option>';
1009
+ projects.forEach((project) => {
1010
+ const optionElement = document.createElement("option");
1011
+ optionElement.value = project.id || project.value || project;
1012
+ optionElement.textContent =
1013
+ project.name || project.label || project;
1014
+ projectSelect.appendChild(optionElement);
1015
+ });
1016
+ }
1017
+ }
1018
+ });
1019
+ }
1020
+ if (engProjectSelect) {
1021
+ engProjectSelect.addEventListener("change", () => {
1022
+ currentState.engTicketProject = engProjectSelect.value;
1023
+ // Update text color based on selection
1024
+ engProjectSelect.style.color = engProjectSelect.value ? "" : "#9ca3af";
1025
+ // Clear custom fields when project changes
1026
+ currentState.engTicketCustomFields = {};
1027
+ // Update issue types when project changes
1028
+ const integrationData = getIntegrationData();
1029
+ if (integrationData && engTypeSelect) {
1030
+ updateIssueTypeOptions(engTypeSelect, engProjectSelect.value);
1031
+ // Update state with the selected/default issue type
1032
+ currentState.engTicketIssueType = engTypeSelect.value;
1033
+ }
1034
+ // Handle reporter field for Jira
1035
+ if (integrationData &&
1036
+ integrationData.provider?.toLowerCase() === "jira" &&
1037
+ integrationData.jiraReporterAccountId &&
1038
+ engProjectSelect.value) {
1039
+ const fields = getFieldsForProject(engProjectSelect.value, currentState.engTicketIssueType);
1040
+ const reporterField = fields.find((f) => f.fieldId === "reporter");
1041
+ if (reporterField) {
1042
+ currentState.engTicketCustomFields["reporter"] =
1043
+ integrationData.jiraReporterAccountId;
1044
+ }
1045
+ }
1046
+ // Render dynamic fields for the selected project and issue type
1047
+ renderDynamicFields(engProjectSelect.value, currentState.engTicketIssueType);
1048
+ });
1049
+ }
1050
+ if (engPrioritySelect) {
1051
+ engPrioritySelect.addEventListener("change", () => {
1052
+ currentState.engTicketPriority = Number(engPrioritySelect.value);
1053
+ });
1054
+ }
1055
+ if (engLabelsContainer) {
1056
+ // Set up event listeners for custom multiselect
1057
+ setupCustomMultiSelectListeners("sf-eng-ticket-labels", (selectedValues) => {
1058
+ currentState.engTicketLabels = selectedValues;
1059
+ });
1060
+ }
1061
+ if (engTypeSelect) {
1062
+ engTypeSelect.addEventListener("change", () => {
1063
+ currentState.engTicketIssueType = engTypeSelect.value;
1064
+ // Update text color based on selection
1065
+ engTypeSelect.style.color = engTypeSelect.value ? "" : "#9ca3af";
1066
+ // Re-render dynamic fields when issue type changes
1067
+ const engProjectSelect = document.getElementById("sf-eng-ticket-project");
1068
+ if (engProjectSelect && engProjectSelect.value) {
1069
+ // Clear custom fields except reporter
1070
+ const reporterValue = currentState.engTicketCustomFields["reporter"];
1071
+ currentState.engTicketCustomFields = {};
1072
+ if (reporterValue) {
1073
+ currentState.engTicketCustomFields["reporter"] = reporterValue;
1074
+ }
1075
+ renderDynamicFields(engProjectSelect.value, engTypeSelect.value);
1076
+ }
1077
+ });
1078
+ }
1079
+ // Dynamic fields event delegation
1080
+ const dynamicFieldsContainer = document.getElementById("sf-dynamic-fields-container");
1081
+ if (dynamicFieldsContainer) {
1082
+ dynamicFieldsContainer.addEventListener("input", (e) => {
1083
+ const target = e.target;
1084
+ if (target.classList.contains("sf-dynamic-field")) {
1085
+ const fieldId = target.dataset.fieldId;
1086
+ if (fieldId) {
1087
+ // Handle different input types
1088
+ if (target.type === "checkbox") {
1089
+ currentState.engTicketCustomFields[fieldId] = target.checked;
1090
+ }
1091
+ else if (target.type === "number") {
1092
+ currentState.engTicketCustomFields[fieldId] =
1093
+ parseFloat(target.value) || null;
1094
+ }
1095
+ else {
1096
+ currentState.engTicketCustomFields[fieldId] = target.value;
1097
+ }
1098
+ }
1099
+ }
1100
+ });
1101
+ dynamicFieldsContainer.addEventListener("change", (e) => {
1102
+ const target = e.target;
1103
+ if (target.classList.contains("sf-dynamic-field")) {
1104
+ const fieldId = target.dataset.fieldId;
1105
+ if (fieldId) {
1106
+ currentState.engTicketCustomFields[fieldId] = target.value;
1107
+ }
1108
+ // Update text color for select elements (to remove placeholder gray)
1109
+ if (target.tagName === "SELECT") {
1110
+ const selectElement = target;
1111
+ selectElement.style.color = selectElement.value ? "" : "#9ca3af";
1112
+ }
1113
+ }
1114
+ });
1115
+ }
620
1116
  // Start recording
621
1117
  if (recordBtn) {
622
1118
  recordBtn.onclick = () => {
@@ -635,6 +1131,11 @@ function bindListeners() {
635
1131
  ?.value || "";
636
1132
  const mode = currentState.mode;
637
1133
  currentState.description = desc;
1134
+ // Validate issue name if creating an issue
1135
+ if (currentState.createIssue && !currentState.issueName.trim()) {
1136
+ alert("Issue title is required when creating an issue.");
1137
+ return;
1138
+ }
638
1139
  let startTimestamp;
639
1140
  let endTimestamp;
640
1141
  if (mode === "startnow") {
@@ -647,8 +1148,55 @@ function bindListeners() {
647
1148
  endTimestamp = Date.now();
648
1149
  startTimestamp = endTimestamp - delta;
649
1150
  }
650
- closeModal();
651
- createTriage(`${startTimestamp}`, `${endTimestamp}`, desc);
1151
+ // Read values from DOM inputs BEFORE closing modal (so elements still exist)
1152
+ if (currentState.createIssue) {
1153
+ const issueNameInput = document.getElementById("sf-issue-name");
1154
+ const engTeamSelect = document.getElementById("sf-eng-ticket-team");
1155
+ const engProjectSelect = document.getElementById("sf-eng-ticket-project");
1156
+ const engPrioritySelect = document.getElementById("sf-eng-ticket-priority");
1157
+ const engTypeSelect = document.getElementById("sf-eng-ticket-type");
1158
+ const issueName = issueNameInput?.value || "";
1159
+ // Use the triage description for issue description
1160
+ const issueDescription = desc;
1161
+ const engTicketTeam = engTeamSelect?.value || "";
1162
+ const engTicketProject = engProjectSelect?.value || "";
1163
+ const engTicketPriority = engPrioritySelect
1164
+ ? Number(engPrioritySelect.value)
1165
+ : 0;
1166
+ // Labels come from currentState since we update it in the event listener
1167
+ const engTicketLabels = currentState.engTicketLabels;
1168
+ const engTicketIssueType = engTypeSelect?.value || "";
1169
+ // Read dynamic field values from DOM (non-multiselect fields)
1170
+ const engTicketCustomFields = {
1171
+ ...currentState.engTicketCustomFields,
1172
+ };
1173
+ const dynamicFieldElements = document.querySelectorAll(".sf-dynamic-field");
1174
+ dynamicFieldElements.forEach((element) => {
1175
+ const fieldElement = element;
1176
+ const fieldId = fieldElement.dataset.fieldId;
1177
+ if (fieldId) {
1178
+ if (fieldElement.type === "checkbox") {
1179
+ engTicketCustomFields[fieldId] = fieldElement.checked;
1180
+ }
1181
+ else if (fieldElement.type === "number") {
1182
+ engTicketCustomFields[fieldId] =
1183
+ parseFloat(fieldElement.value) || null;
1184
+ }
1185
+ else if (!fieldElement.classList.contains("sf-custom-multiselect")) {
1186
+ // Only read from DOM if it's not a custom multiselect (those are already in state)
1187
+ engTicketCustomFields[fieldId] = fieldElement.value;
1188
+ }
1189
+ }
1190
+ });
1191
+ closeModal();
1192
+ // Create triage + issue (with optional engineering ticket)
1193
+ createTriageAndIssue(`${startTimestamp}`, `${endTimestamp}`, desc, issueName, issueDescription, currentState.createEngTicket, engTicketTeam, engTicketProject, engTicketPriority, engTicketLabels, engTicketIssueType, engTicketCustomFields);
1194
+ }
1195
+ else {
1196
+ closeModal();
1197
+ // Create triage only
1198
+ createTriage(`${startTimestamp}`, `${endTimestamp}`, desc);
1199
+ }
652
1200
  }
653
1201
  });
654
1202
  }
@@ -683,11 +1231,11 @@ function startCountdownThenRecord() {
683
1231
  clearInterval(interval);
684
1232
  document.body.removeChild(overlay);
685
1233
  // Begin recording
686
- recordingStartTime = Date.now();
687
- isRecording = true;
1234
+ setRecordingStartTime(Date.now());
1235
+ setIsRecording(true);
688
1236
  // Enable function span tracking for this recording session
689
1237
  try {
690
- const { enableFunctionSpanTracking } = await import("./websocket");
1238
+ const { enableFunctionSpanTracking } = await import("../websocket");
691
1239
  enableFunctionSpanTracking();
692
1240
  }
693
1241
  catch (e) {
@@ -739,7 +1287,7 @@ function showFloatingTimer() {
739
1287
  const timerEl = timer.querySelector("#sf-recording-timer");
740
1288
  if (!timerEl)
741
1289
  return;
742
- timerInterval = setInterval(() => {
1290
+ const interval = setInterval(() => {
743
1291
  const delta = Date.now() - (recordingStartTime ?? Date.now());
744
1292
  const mins = Math.floor(delta / 60000)
745
1293
  .toString()
@@ -749,16 +1297,19 @@ function showFloatingTimer() {
749
1297
  .padStart(2, "0");
750
1298
  timerEl.textContent = `${mins}:${secs}`;
751
1299
  }, 1000);
1300
+ setTimerInterval(interval);
752
1301
  }
753
1302
  async function stopRecording() {
754
- recordingEndTime = Date.now();
755
- isRecording = false;
756
- if (timerInterval)
1303
+ setRecordingEndTime(Date.now());
1304
+ setIsRecording(false);
1305
+ if (timerInterval) {
757
1306
  clearInterval(timerInterval);
1307
+ setTimerInterval(null);
1308
+ }
758
1309
  document.getElementById("sf-recording-indicator")?.remove();
759
1310
  // Disable function span tracking after recording stops
760
1311
  try {
761
- const { disableFunctionSpanTracking } = await import("./websocket");
1312
+ const { disableFunctionSpanTracking } = await import("../websocket");
762
1313
  disableFunctionSpanTracking();
763
1314
  }
764
1315
  catch (e) {
@@ -812,27 +1363,53 @@ function reopenModalAfterStop() {
812
1363
  }
813
1364
  async function createTriage(startTimestamp, endTimestamp, description) {
814
1365
  try {
815
- showTriageStatusModal(true);
1366
+ showStatusModal(true);
816
1367
  const response = await createTriageFromRecorder(ReportIssueContext.apiKey, ReportIssueContext.backendApi, getSessionIdSafely(), startTimestamp, endTimestamp, description);
817
1368
  const triageId = response?.data?.createTriageFromRecorder?.id;
818
1369
  if (triageId) {
819
- showTriageStatusModal(false, triageId);
1370
+ showStatusModal(false, { type: "triage", id: triageId });
820
1371
  }
821
1372
  else {
822
1373
  console.error("No Triage ID returned from backend.");
823
- showTriageStatusModal(false, null); // fallback behavior
1374
+ showStatusModal(false, null);
824
1375
  }
825
1376
  }
826
1377
  catch (error) {
827
1378
  console.error("Error creating triage:", error);
828
- showTriageStatusModal(false, null);
1379
+ showStatusModal(false, null);
1380
+ }
1381
+ }
1382
+ async function createTriageAndIssue(startTimestamp, endTimestamp, description, issueName, issueDescription, createEngTicket, engTicketTeam, engTicketProject, engTicketPriority, engTicketLabels, engTicketIssueType, engTicketCustomFields) {
1383
+ try {
1384
+ showStatusModal(true);
1385
+ const response = await createTriageAndIssueFromRecorder(ReportIssueContext.apiKey, ReportIssueContext.backendApi, getSessionIdSafely(), startTimestamp, endTimestamp, description, issueName, issueDescription, createEngTicket, engTicketTeam, engTicketProject, engTicketPriority, engTicketLabels, engTicketIssueType, engTicketCustomFields);
1386
+ const issueId = response?.data?.createTriageAndIssueFromRecorder?.id;
1387
+ if (issueId) {
1388
+ showStatusModal(false, { type: "issue", id: issueId });
1389
+ }
1390
+ else {
1391
+ console.error("No Issue ID returned from backend.");
1392
+ showStatusModal(false, null);
1393
+ }
1394
+ }
1395
+ catch (error) {
1396
+ console.error("Error creating triage and issue:", error);
1397
+ showStatusModal(false, null);
829
1398
  }
830
1399
  }
831
- function showTriageStatusModal(isLoading, triageId) {
1400
+ function showStatusModal(isLoading, result) {
1401
+ const triageId = result?.type === "triage" ? result.id : undefined;
1402
+ const issueId = result?.type === "issue" ? result.id : undefined;
1403
+ showTriageStatusModal(isLoading, triageId, issueId);
1404
+ }
1405
+ function showTriageStatusModal(isLoading, triageId, issueId) {
832
1406
  removeExistingModals();
833
- const triageUrl = triageId
834
- ? `${ReportIssueContext.triageBaseUrl}/triage/${triageId}?from=inAppReportIssue`
835
- : "";
1407
+ // Prefer issue URL if available, otherwise use triage URL
1408
+ const resultUrl = issueId
1409
+ ? `${ReportIssueContext.triageBaseUrl}/issues/${issueId}?from=inAppReportIssue`
1410
+ : triageId
1411
+ ? `${ReportIssueContext.triageBaseUrl}/triage/${triageId}?from=inAppReportIssue`
1412
+ : "";
836
1413
  const container = document.createElement("div");
837
1414
  container.id = "sf-triage-status-modal";
838
1415
  Object.assign(container.style, {
@@ -924,7 +1501,7 @@ function showTriageStatusModal(isLoading, triageId) {
924
1501
  else {
925
1502
  copyBtn.disabled = false;
926
1503
  copyBtn.addEventListener("click", () => {
927
- navigator.clipboard.writeText(triageUrl).then(() => {
1504
+ navigator.clipboard.writeText(resultUrl).then(() => {
928
1505
  const copiedStatus = document.getElementById("sf-copied-status");
929
1506
  if (copiedStatus)
930
1507
  copiedStatus.style.display = "flex";
@@ -932,8 +1509,8 @@ function showTriageStatusModal(isLoading, triageId) {
932
1509
  });
933
1510
  viewBtn.disabled = false;
934
1511
  viewBtn.addEventListener("click", () => {
935
- if (triageId) {
936
- window.open(triageUrl, "_blank");
1512
+ if (triageId || issueId) {
1513
+ window.open(resultUrl, "_blank");
937
1514
  }
938
1515
  });
939
1516
  // Auto-hide after success