1// Response Handler Module 2const ResponseHandler = (function ($) { 3 /** 4 * Helper function to extract HTML content from various response structures 5 */ 6 function extractHtmlContent(responseData) { 7 if (!responseData) return ""; 8 9 // Try various paths where HTML data might be 10 let htmlData = 11 responseData.response?.response?.data.html || // Nested structure from API 12 responseData.response?.data.html || // Direct API response 13 responseData.data.html || // Simple structure 14 ""; 15 16 // If we still have an object, try to stringify it or extract 'data' property 17 if (typeof htmlData === 'object' && htmlData !== null) { 18 if (htmlData.data) { 19 htmlData = htmlData.data.html; 20 } else { 21 // Last resort: stringify the object 22 htmlData = JSON.stringify(htmlData, null, 2); 23 } 24 } 25 26 return htmlData || ""; 27 } 28 29 function showResponseDialog(data) { 30 const responses = data.responses || []; 31 const bulkEmailPayloads = Array.isArray(data.bulk_email_payloads) 32 ? data.bulk_email_payloads 33 : []; 34 let htmlContent = ""; 35 let pdfResponses = []; 36 37 // First pass: categorize responses by type 38 responses.forEach((response) => { 39 const responseData = response.response; 40 if (responseData?.type === "html") { 41 const htmlData = extractHtmlContent(responseData); 42 htmlContent += 43 (htmlContent ? "\n\n<!-- Next Response -->\n\n" : "") + htmlData; 44 } else if (responseData?.type === "pdf") { 45 pdfResponses.push(response); 46 } 47 }); 48 49 if (!htmlContent && bulkEmailPayloads.length > 0) { 50 htmlContent = bulkEmailPayloads 51 .map((payload, index) => { 52 const subject = payload.email_subject || `Recipient ${index + 1}`; 53 const recipient = payload.email || ""; 54 const body = payload.email_content || ""; 55 if (!body) { 56 return ""; 57 } 58 59 return ` 60 <div class="bulk-preview-item" style="margin-bottom:24px;"> 61 <div style="padding:10px 12px; background:#f6f7f7; border:1px solid #dcdcde; border-radius:4px; margin-bottom:12px;"> 62 <strong>Preview ${index + 1}</strong><br> 63 <span>To: ${recipient}</span><br> 64 <span>Subject: ${subject}</span> 65 </div> 66 <div>${body}</div> 67 </div> 68 `; 69 }) 70 .filter(Boolean) 71 .join("\n<hr style=\"margin:24px 0; border-top:1px solid #ddd;\">\n"); 72 } 73 74 // Create single dialog with combined content 75 const dialogHtml = createDialogHtml({ 76 hasHtml: htmlContent !== "", 77 htmlContent: htmlContent, 78 allResponses: responses, 79 pdfResponses: pdfResponses, 80 }); 81 82 updatePopupContent(dialogHtml); 83 84 if (htmlContent) { 85 initializePreview(); 86 } 87 88 setupEventHandlers({ 89 responses: responses, 90 button_id: data.button_id, 91 post_id: data.post_id, 92 params: data.params, 93 session_id: data.session_id, 94 bulk_recipients: data.bulk_recipients || [], 95 bulk_email_payloads: bulkEmailPayloads, 96 }); 97 } 98 99 function createDialogHtml(data) { 100 const { hasHtml, htmlContent, pdfResponses } = data; 101 102 return ` 103 <div class="outliny-response-dialog" style="height: 90%; display: flex; flex-direction: column;"> 104 <h3>Response</h3> 105 106 <!-- Content Section (70% height) --> 107 <div class="content-section" style="height: 70%; margin-bottom: 15px;"> 108 ${hasHtml 109 ? createHtmlEditorHtml(htmlContent) 110 : createPdfViewerHtml() 111 } 112 </div> 113 114 <!-- Horizontal Divider --> 115 <hr style="margin: 0 -20px 15px -20px; border-top: 1px solid #ddd;"> 116 117 <!-- Attachments Section (30% height) --> 118 <div class="attachments-section" style="height: 30%;"> 119 ${createAttachmentsSectionHtml(pdfResponses)} 120 </div> 121 <div style="display: flex; justify-content: center; margin: 10px; padding: 10px;"> 122 <button class="button button-close" style="margin-right: 10px;">Close</button> 123 ${hasHtml 124 ? '<button class="button button-primary execute-final-action">Perform Final Action</button>' 125 : "" 126 } 127 </div> 128 </div> 129 `; 130 } 131 132 function createHtmlEditorHtml(html) { 133 return ` 134 <div class="email-editor" style="display: flex; height: 50vh; !important"> 135 <div class="text-editor" style="flex: 1; padding-right: 10px; display: flex; flex-direction: column;"> 136 <h4 style="margin: 0 0 10px 0;">Email Content</h4> 137 <textarea 138 class="email-content-editor" 139 style="width: 100%; flex: 1; resize: none; min-height: 0;" 140 >${html}</textarea> 141 </div> 142 <div class="html-preview" style="flex: 1; display: flex; flex-direction: column;"> 143 <h4 style="margin: 0 0 10px 0;">Preview</h4> 144 <iframe 145 class="preview-frame" 146 style="width: 100%; flex: 1; border: 1px solid #ddd; background: white; min-height: 0;" 147 ></iframe> 148 </div> 149 </div> 150 `; 151 } 152 153 function createPdfViewerHtml() { 154 return ` 155 <div class="pdf-viewer" style="height: 100%;"> 156 <h4>PDF Document</h4> 157 <div style="height: calc(100% - 40px); border: 1px solid #ddd; padding: 10px; background: #f9f9f9;"> 158 <p>PDF file will be available in attachments section</p> 159 </div> 160 </div> 161 `; 162 } 163 164 function createAttachmentsSectionHtml(pdfResponses) { 165 return ` 166 <div class="attachments-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> 167 <h4 style="margin: 0;">Attachments</h4> 168 <div class="attachment-actions"> 169 <input type="file" id="add-attachment" style="display: none;" multiple> 170 <button class="button add-attachment-btn">Add Attachment</button> 171 </div> 172 </div> 173 <div class="attachments-list" style="height: calc(100% - 50px); overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: #f9f9f9;"> 174 ${generateAttachmentsList(pdfResponses)} 175 </div> 176 `; 177 } 178 179 function generateAttachmentsList(pdfResponses) { 180 if (!pdfResponses || pdfResponses.length === 0) { 181 return '<p class="no-attachments" style="text-align: center; color: #666;">No attachments added</p>'; 182 } 183 184 const attachmentItems = pdfResponses.map((response, index) => { 185 const responseData = response.response; 186 // Ensure we have a proper URL by checking both response structures 187 const fileUrl =
188 responseData?.file_url || responseData?.response?.file_url; 189 let filename = 190 responseData?.file_name || responseData?.response?.file_name; 191 filename = filename ?? "File " + (index + 1); 192 // Make sure we have an absolute URL 193 const url = fileUrl.startsWith("http") 194 ? fileUrl 195 : window.location.origin + fileUrl; 196 197 return { 198 name: `${filename || "Response"}.pdf`, 199 url: url, 200 isPdf: true, 201 actionId: response.action_id, 202 originalName: filename, // Use the actual filename from the server 203 }; 204 }); 205 206 return generateAttachmentsListHtml(attachmentItems); 207 } 208 209 function generateAttachmentsListHtml(attachmentItems) { 210 return ` 211 <ul class="attachments-items" style="list-style: none; padding: 0; margin: 0;"> 212 ${attachmentItems 213 .map( 214 (attachment, index) => ` 215 <li class="attachment-item" 216 data-original-name="${attachment.originalName}" 217 data-url="${attachment.url}" 218 data-display-name="${attachment.name}" 219 style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: white; margin-bottom: 5px; border: 1px solid #eee; border-radius: 3px;"> 220 <span class="attachment-name" style="display: flex; align-items: center;"> 221 <svg style="width: 16px; height: 16px; margin-right: 8px;" viewBox="0 0 24 24"> 222 <path fill="currentColor" d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M9.5 11.5C9.5 12.3 8.8 13 8 13H7V15H5.5V9H8C8.8 9 9.5 9.7 9.5 10.5V11.5M14.5 13.5C14.5 14.3 13.8 15 13 15H10.5V9H13C13.8 9 14.5 9.7 14.5 10.5V13.5M18.5 10.5H17V11.5H18.5V13H17V15H15.5V9H18.5V10.5M7 11.5H8V10.5H7V11.5M13 13.5V10.5H12V13.5H13Z"/> 223 </svg> 224 ${attachment.name} 225 </span> 226 <div class="attachment-actions"> 227 <button class="button rename-attachment" data-index="${index}" style="margin-right: 8px;"> 228 Rename 229 </button> 230 <a href="${attachment.url}" 231 download="${attachment.name}" 232 class="button button-primary download-attachment" 233 style="margin-right: 8px;" 234 target="_blank"> 235 Download 236 </a> 237 <button class="button button-link-delete remove-attachment" data-index="${index}" style="color: #dc3545;"> 238 Remove 239 </button> 240 </div> 241 </li> 242 ` 243 ) 244 .join("")} 245 </ul> 246 `; 247 } 248 249 function updatePopupContent(dialogHtml) { 250 const popupOverlay = $(".outliny-popup-overlay"); 251 const outlinyPopup = popupOverlay.find(".outliny-popup"); 252 const popupContent = popupOverlay.find(".outliny-popup-content"); 253 const loadingDiv = popupContent.find(".outliny-popup-loading"); 254 const responseDiv = popupContent.find(".outliny-popup-response"); 255 256 loadingDiv.hide(); 257 responseDiv.html(dialogHtml).show(); 258 259 updatePopupStyles(popupOverlay, popupContent, outlinyPopup); 260 } 261 262 function updatePopupStyles(popupOverlay, popupContent, outlinyPopup) { 263 outlinyPopup.css({ 264 width: "90vw", 265 height: "90vh", 266 "max-width": "90vw", 267 "max-height": "90vh", 268 }); 269 popupOverlay.css({ 270 display: "flex", 271 "align-items": "center", 272 "justify-content": "center", 273 background: "rgba(0, 0, 0, 0.5)", 274 position: "fixed", 275 top: 0, 276 left: 0, 277 right: 0, 278 bottom: 0, 279 "z-index": 999999, 280 padding: "20px", 281 });
282 283 popupContent.css({ 284 height: "90vh", 285 background: "white", 286 padding: "20px", 287 "border-radius": "4px", 288 "box-shadow": "0 2px 8px rgba(0, 0, 0, 0.2)", 289 overflow: "hidden", 290 position: "relative", 291 margin: "0 auto", 292 }); 293 } 294 295 function initializePreview() { 296 const textarea = $(".email-content-editor"); 297 const iframe = $(".preview-frame"); 298 const iframeDoc = iframe[0].contentWindow.document; 299 300 updatePreview(textarea.val(), iframeDoc); 301 302 let timeout; 303 textarea.on("input", function () { 304 clearTimeout(timeout); 305 timeout = setTimeout(() => { 306 updatePreview($(this).val(), iframeDoc); 307 }, 300); 308 }); 309 } 310 311 function updatePreview(content, iframeDoc) { 312 iframeDoc.open(); 313 iframeDoc.write(` 314 <!DOCTYPE html> 315 <html> 316 <head> 317 <meta charset="UTF-8"> 318 <style> 319 body { 320 font-family: Arial, sans-serif; 321 padding: 20px; 322 margin: 0; 323 line-height: 1.6; 324 } 325 img { max-width: 100%; height: auto; } 326 table { border-collapse: collapse; } 327 td, th { padding: 8px; border: 1px solid #ddd; } 328 </style> 329 </head> 330 <body>${content}</body> 331 </html> 332 `); 333 iframeDoc.close(); 334 } 335 336 function setupEventHandlers(data) { 337 // Add attachment button handler 338 $(".add-attachment-btn").on("click", function () { 339 $("#add-attachment").trigger("click"); 340 }); 341 342 // File selection handler 343 $("#add-attachment").on("change", handleFileSelection); 344 345 // Remove attachment handler 346 $(document).on("click", ".remove-attachment", function () { 347 const item = $(this).closest(".attachment-item"); 348 handleAttachmentRemoval(item); 349 }); 350 351 // Rename attachment handler 352 $(document).on("click", ".rename-attachment", function () { 353 const item = $(this).closest(".attachment-item"); 354 const originalName = item.data("original-name"); 355 const currentName = item.find(".attachment-name").text().trim(); 356 showRenameDialog(item, originalName, currentName, data); 357 }); 358 359 // Download attachment handler 360 $(document).on("click", ".download-attachment", handleDownload); 361 362 // Final action handler - ensure older handlers are removed before binding 363 $(document) 364 .off("click.responseHandler", ".execute-final-action") 365 .on("click.responseHandler", ".execute-final-action", function () { 366 handleFinalAction(data); 367 }); 368 369 // Add close button handler 370 $(".button-close").on("click", function () { 371 const popupOverlay = $(".outliny-popup-overlay"); 372 const popupContent = popupOverlay.find(".outliny-popup-content"); 373 const loadingDiv = popupContent.find(".outliny-popup-loading"); 374 const responseDiv = popupContent.find(".outliny-popup-response"); 375 376 // Hide the overlay 377 popupOverlay.css("display", "none"); 378 379 // Reset popup styles for future use 380 const outlinyPopup = popupOverlay.find(".outliny-popup"); 381 outlinyPopup.css({ 382 width: "90%", 383 "max-width": "600px", 384 height: "auto", 385 "max-height": "unset", 386 }); 387 388 popupContent.css({ 389 height: "auto", 390 "min-height": "200px", 391 "max-width": "600px", 392 width: "unset", 393 }); 394 395 // Reset content sections 396 loadingDiv.show(); 397 responseDiv.hide().empty(); 398 }); 399 } 400 401 function handleFinalAction(data) { 402 const emailContent = $(".email-content-editor").val(); 403 const attachments = []; 404 405 // Collect all attachments 406 $(".attachment-item").each(function () { 407 const $item = $(this); 408 const displayName = $item.data("display-name"); 409 const originalName = $item.data("original-name"); 410 const url = $item.data("url"); 411 412 attachments.push({ 413 name: displayName, 414 originalName: originalName, 415 url: url, 416 isPdf: true, 417 }); 418 });
419 420 // Get references to UI elements 421 const popupOverlay = $(".outliny-popup-overlay"); 422 const popupContent = popupOverlay.find(".outliny-popup-content"); 423 const loadingDiv = popupContent.find(".outliny-popup-loading"); 424 const responseDiv = popupContent.find(".outliny-popup-response"); 425 426 // Show loading state with improved visibility 427 popupOverlay.show(); 428 popupContent.css({ 429 height: "auto", 430 "min-height": "200px", 431 width: "unset", 432 }); 433 // Update loading message to indicate email is being sent 434 loadingDiv.find("p").text("Sending email, please wait..."); 435 loadingDiv.show(); 436 responseDiv.hide().empty(); 437 438 // Add a spinner if it doesn't exist 439 if (loadingDiv.find(".spinner").length === 0) { 440 loadingDiv.prepend( 441 '<span class="spinner is-active" style="float: none; margin: 0 auto 10px; display: block;"></span>' 442 ); 443 } 444 445 // Process the action - with a slight delay to ensure the UI updates are visible 446 setTimeout(() => { 447 window.OutlinyProcessor.processButtonAction( 448 data.button_id, 449 data.post_id, 450 { 451 email_content: encodeURIComponent(emailContent), 452 attachments: JSON.stringify(attachments), 453 bulk_recipients: JSON.stringify(data.bulk_recipients || []), 454 bulk_email_payloads: JSON.stringify(data.bulk_email_payloads || []), 455 stage: "response_shown", 456 params: data.params, 457 session_id: data.session_id, 458 } 459 ); 460 }, 100); 461 } 462 463 function showRenameDialog(item, originalName, currentName, data) { 464 // Validate originalName 465 if (!originalName) { 466 console.error("Original filename is missing:", { 467 item, 468 originalName, 469 currentName, 470 }); 471 alert("Cannot rename file: Original filename is missing"); 472 return; 473 } 474 475 // Extract the current name without extension 476 const nameWithoutExt = currentName.replace(/\.pdf$/i, ""); 477 478 const dialog = $(` 479 <div class="rename-dialog" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); z-index: 1000000;"> 480 <h3>Rename File</h3> 481 <div style="margin-bottom: 15px;"> 482 <input type="text" class="new-filename" value="${nameWithoutExt}" style="width: calc(100% - 45px); padding: 5px;"> 483 <span style="padding: 5px; background: #f0f0f0; border: 1px solid #ddd; margin-left: -1px;">.pdf</span> 484 </div> 485 <div style="display: flex; justify-content: flex-end;"> 486 <button class="button cancel-rename" style="margin-right: 8px;">Cancel</button> 487 <button class="button button-primary confirm-rename">Rename</button> 488 </div> 489 </div> 490 `); 491 492 const overlay = $( 493 '<div class="rename-overlay" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999999;"></div>' 494 ); 495 496 $("body").append(overlay).append(dialog); 497 498 dialog.find(".cancel-rename").on("click", function () { 499 dialog.remove(); 500 overlay.remove(); 501 }); 502 503 dialog.find(".confirm-rename").on("click", function () { 504 const newNameWithoutExt = dialog.find(".new-filename").val().trim(); 505 if (!newNameWithoutExt) { 506 alert("Please enter a new filename"); 507 return; 508 } 509 510 const newName = newNameWithoutExt + ".pdf"; 511 console.log("Updating display name:", { originalName, newName }); 512 513 // Update only the display elements, keep the original file 514 item.find(".attachment-name").html(` 515 <svg style="width: 16px; height: 16px; margin-right: 8px;" viewBox="0 0 24 24"> 516 <path fill="currentColor" d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M9.5 11.5C9.5 12.3 8.8 13 8 13H7V15H5.5V9H8C8.8 9 9.5 9.7 9.5 10.5V11.5M14.5 13.5C14.5 14.3 13.8 15 13 15H10.5V9H13C13.8 9 14.5 9.7 14.5 10.5V13.5M18.5 10.5H17V11.5H18.5V13H17V15H15.5V9H18.5V10.5M7 11.5H8V10.5H7V11.5M13 13.5V10.5H12V13.5H13Z"/> 517 </svg> 518 ${newName} 519 `); 520 item.find(".download-attachment").attr("download", newName); 521 item.data("display-name", newName); 522 523 dialog.remove(); 524 overlay.remove(); 525 });
526 } 527 528 function handleFileSelection(e) { 529 const files = e.target.files; 530 const attachmentsList = $(".attachments-list"); 531 532 attachmentsList.find(".no-attachments").remove(); 533 534 if (!attachmentsList.find(".attachments-items").length) { 535 attachmentsList.append( 536 '<ul class="attachments-items" style="list-style: none; padding: 0; margin: 0;"></ul>' 537 ); 538 } 539 540 Array.from(files).forEach((file) => { 541 const newItem = createAttachmentItem(file); 542 attachmentsList.find(".attachments-items").append(newItem); 543 }); 544 } 545 546 function createAttachmentItem(file) { 547 return $(` 548 <li class="attachment-item" style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: white; margin-bottom: 5px; border: 1px solid #eee; border-radius: 3px;"> 549 <span class="attachment-name" style="display: flex; align-items: center;"> 550 <svg style="width: 16px; height: 16px; margin-right: 8px;" viewBox="0 0 24 24"> 551 <path fill="currentColor" d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/> 552 </svg> 553 ${file.name} 554 </span> 555 <button class="button-link-delete remove-attachment" style="color: #dc3545;">Remove</button> 556 </li> 557 `); 558 } 559 560 function handleAttachmentRemoval(item) { 561 item.fadeOut(200, function () { 562 $(this).remove(); 563 564 const attachmentsList = $(".attachments-list"); 565 if (!attachmentsList.find(".attachment-item").length) { 566 attachmentsList.html( 567 '<p class="no-attachments" style="text-align: center; color: #666;">No attachments added</p>' 568 ); 569 } 570 }); 571 } 572 573 // Add this new function to handle downloads 574 function handleDownload(e) { 575 e.preventDefault(); 576 const link = $(this); 577 const url = link.attr("href"); 578 const filename = link.attr("download"); 579 580 // Create a temporary link and trigger download 581 const tempLink = document.createElement("a"); 582 tempLink.style.display = "none"; 583 tempLink.href = url; 584 tempLink.setAttribute("download", filename); 585 document.body.appendChild(tempLink); 586 tempLink.click(); 587 document.body.removeChild(tempLink); 588 } 589 590 return { 591 showResponseDialog: showResponseDialog, 592 initializePreview: initializePreview, 593 handleDownload: handleDownload, 594 }; 595})(jQuery); 596 597// Export the module 598window.OutlinyResponseHandler = ResponseHandler;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.