// The Lucia project. // Copyright 2023-2026 DSP, inc. All rights reserved. // Authors: // chiayin.kuo@dsp.im (chiayin), 2023/1/31 // imacat.yang@dsp.im (imacat), 2023/9/23 // cindy.chang@dsp.im (Cindy Chang), 2024/5/30 /** @module alertModal SweetAlert2 modal dialogs for user interactions. */ import Swal from "sweetalert2"; import { useAllMapDataStore } from "@/stores/allMapData"; import { useConformanceStore } from "@/stores/conformance"; import { useFilesStore } from "@/stores/files"; import { usePageAdminStore } from "@/stores/pageAdmin"; import { useModalStore } from "@/stores/modal"; import { escapeHtml } from "@/utils/escapeHtml.js"; const customClass = { container: "!z-[99999]", popup: "!w-[564px]", title: "!text-xl !font-semibold !mb-2", htmlContainer: "!text-sm !font-normal !h-full !mb-4 !leading-5", inputLabel: "!text-sm !font-normal", input: "!h-8 !text-sm !font-normal !shadow-inner !shadow-black !border-neutral-200", validationMessage: "!bg-neutral-10 !text-danger", confirmButton: "!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px]", cancelButton: "!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] ", }; /** * Shows a modal dialog to save a new filter with a user-provided name. * * @param {Function} addFilterId - Backend API function to create the filter. * @param {Function|null} [next=null] - Vue Router next() guard callback. * @returns {Promise} True if the filter was saved, false otherwise. */ export async function saveFilter(addFilterId, next = null) { let fileName = ""; const pageAdminStore = usePageAdminStore(); const { value, isConfirmed } = await Swal.fire({ title: "SAVE NEW FILTER", input: "text", inputPlaceholder: "Enter Filter Name.", inputValue: fileName, inputAttributes: { maxlength: 200, }, inputValidator: (value) => { if (!value) return "You need to write something!"; fileName = value; }, icon: "info", iconHtml: 'cloud_upload', iconColor: "#0099FF", reverseButtons: true, confirmButtonColor: "#0099FF", showCancelButton: true, cancelButtonColor: "#94a3b8", customClass: customClass, }); // Determine whether to redirect based on the return value if (isConfirmed) { // Save succeeded await addFilterId(fileName); // Show save complete notification if (value) { // Example of value: yes savedSuccessfully(value); } // Clear the input field fileName = ""; return true; } else { // Clicked cancel or outside the dialog; save failed. pageAdminStore.keepPreviousPage(); // Not every time we have nontrivial next value if (next !== null) { next(false); } return false; } } /** * Shows a timed success notification after a file has been saved. * * @param {string} value - The name of the saved file. * @returns {Promise} */ export async function savedSuccessfully(value = "") { await Swal.fire({ title: "SAVE COMPLETE", html: `${escapeHtml(value)} has been saved in Lucia.`, timer: 3000, // Auto-close after 3 seconds showConfirmButton: false, icon: "success", iconColor: "#0099FF", customClass: customClass, }); } /** * Prompts the user to save unsaved filter changes before leaving the * Map page. Handles confirm (save), cancel (discard), and backdrop * (stay) scenarios. * * @param {Function} next - Vue Router next() guard callback. * @param {Function} addFilterId - Backend API function to create the filter. * @param {string} toPath - The destination route path. * @param {Function} [logOut] - Optional logout function to call instead * of navigating. * @returns {Promise} */ export async function leaveFilter(next, addFilterId, toPath, logOut) { const allMapDataStore = useAllMapDataStore(); const pageAdminStore = usePageAdminStore(); const result = await Swal.fire({ title: "SAVE YOUR FILTER?", html: "If you want to continue using this filter in any other page, please select [Yes].", icon: "warning", iconColor: "#FF3366", reverseButtons: true, confirmButtonText: "Yes", confirmButtonColor: "#FF3366", showCancelButton: true, cancelButtonText: "No", cancelButtonColor: "#94a3b8", customClass: customClass, }); if (result.isConfirmed) { if (allMapDataStore.createFilterId) { await allMapDataStore.updateFilter(); if (allMapDataStore.isUpdateFilter) { await savedSuccessfully(allMapDataStore.filterName); } } else { // Dangerous, here shows a modal await saveFilter(addFilterId, next); } logOut ? logOut() : next(toPath); } else if (result.dismiss === "cancel") { // console.log('popup cancel case', ); // Handle page admin issue // console.log("usePageAdminStore.activePage", usePageAdminStore.activePage); pageAdminStore.keepPreviousPage(); allMapDataStore.tempFilterId = null; logOut ? logOut() : next(toPath); } else if (result.dismiss === "backdrop") { // console.log('popup backdrop case', ); // Handle page admin issue // console.log("usePageAdminStore.activePage", usePageAdminStore.activePage); pageAdminStore.keepPreviousPage(); if (!logOut) { next(false); } } } /** * Shows a modal dialog to save a new conformance rule with a * user-provided name. * * @param {Function} addConformanceCreateCheckId - Backend API function * to create the conformance check. * @returns {Promise} True if the rule was saved, false otherwise. */ export async function saveConformance(addConformanceCreateCheckId) { let fileName = ""; const { value, isConfirmed } = await Swal.fire({ title: "SAVE NEW RULE", input: "text", inputPlaceholder: "Enter Rule Name.", inputValue: fileName, inputAttributes: { maxlength: 200, }, inputValidator: (value) => { if (!value) return "You need to write something!"; fileName = value; }, icon: "info", iconHtml: 'cloud_upload', iconColor: "#0099FF", reverseButtons: true, confirmButtonColor: "#0099FF", showCancelButton: true, cancelButtonColor: "#94a3b8", customClass: customClass, }); // Determine whether to redirect based on the return value if (isConfirmed) { // Save succeeded await addConformanceCreateCheckId(fileName); // Show save complete notification if (value) savedSuccessfully(value); // Clear the input field fileName = ""; return true; } else { // Clicked cancel or outside the dialog; save failed. return false; } } /** * Prompts the user to save unsaved conformance rule changes before * leaving the Conformance page. Delegates to helper functions for * confirm, cancel, and backdrop scenarios. * * @param {Function} next - Vue Router next() guard callback. * @param {Function} addConformanceCreateCheckId - Backend API function * to create the conformance check. * @param {string} toPath - The destination route path. * @param {Function} [logOut] - Optional logout function. * @returns {Promise} */ export async function leaveConformance( next, addConformanceCreateCheckId, toPath, logOut, ) { const conformanceStore = useConformanceStore(); const result = await showConfirmationDialog(); if (result.isConfirmed) { await handleConfirmed(conformanceStore, addConformanceCreateCheckId); } else { await handleDismiss(result.dismiss, conformanceStore, next, toPath, logOut); } } /** * Displays the "SAVE YOUR RULE?" confirmation dialog. * @returns {Promise} The SweetAlert2 result object. */ async function showConfirmationDialog() { return Swal.fire({ title: "SAVE YOUR RULE?", icon: "warning", iconColor: "#FF3366", reverseButtons: true, confirmButtonText: "Yes", confirmButtonColor: "#FF3366", showCancelButton: true, cancelButtonText: "No", cancelButtonColor: "#94a3b8", customClass: customClass, }); } /** * Handles the confirmed save action for conformance rules. * @param {Object} conformanceStore - The conformance Pinia store. * @param {Function} addConformanceCreateCheckId - API function to create check. * @returns {Promise} */ async function handleConfirmed(conformanceStore, addConformanceCreateCheckId) { if ( conformanceStore.conformanceFilterCreateCheckId || conformanceStore.conformanceLogCreateCheckId ) { await conformanceStore.updateConformance(); if (conformanceStore.isUpdateConformance) { await savedSuccessfully(conformanceStore.conformanceFileName); } } else { await saveConformance(addConformanceCreateCheckId); } } /** * Handles dismiss actions (cancel or backdrop click) for conformance modals. * @param {string} dismissType - The SweetAlert2 dismiss reason. * @param {Object} conformanceStore - The conformance Pinia store. * @param {Function} next - Vue Router next() guard callback. * @param {string} toPath - The destination route path. * @param {Function} [logOut] - Optional logout function. * @returns {Promise} */ async function handleDismiss( dismissType, conformanceStore, next, toPath, logOut, ) { switch (dismissType) { case "cancel": resetTempCheckId(conformanceStore); logOut ? logOut() : next(toPath); break; case "backdrop": if (!logOut) { next(false); } break; default: break; } } /** * Resets temporary conformance check IDs to null. * @param {Object} conformanceStore - The conformance Pinia store. */ function resetTempCheckId(conformanceStore) { conformanceStore.conformanceFilterTempCheckId = null; conformanceStore.conformanceLogTempCheckId = null; } /** * Shows an error modal for the first stage of upload validation failure. * * @param {string} failureType - The error type from the backend (e.g. * "encoding", "insufficient_columns", "empty", "name_suffix", "mime_type"). * @param {string} failureMsg - The raw error message from the backend. * @param {number} [failureLoc] - The row number where the error occurred. * @returns {Promise} */ export async function uploadFailedFirst(failureType, failureMsg, failureLoc) { // msg: 'not in UTF-8' | 'insufficient columns' | 'the csv file is empty' | 'the filename does not ends with .csv' | 'not a CSV file' // type: 'encoding' | 'insufficient_columns' | 'empty' | 'name_suffix' | mime_type let value = ""; switch (failureType) { case "size": value = "File size exceeds 90MB."; break; case "encoding": value = `Please use UTF-8 for character encoding: (Row #${failureLoc})`; break; case "insufficient_columns": value = "Need at least five columns of data."; break; case "empty": value = "Need at least one record of data."; break; case "name_suffix": case "mime_type": value = "File is not in csv format."; break; default: value = escapeHtml(failureMsg); break; } await Swal.fire({ title: "IMPORT FAILED", html: value, timer: 3000, // Auto-close after 3 seconds showConfirmButton: false, icon: "error", iconColor: "#FF3366", customClass: customClass, }); } /** * Shows an error modal for the second stage of upload validation failure, * listing individual row-level errors. * * @param {Array<{type: string, loc: Array, input: string}>} detail - * Array of error detail objects from the backend. * @returns {Promise} */ export async function uploadFailedSecond(detail) { let srt = ""; let manySrt = ""; detail.forEach((i) => { let content = ""; let key = ""; switch (i.type) { case "too_many": manySrt = "There are more errors."; break; case "unrecognized": content = `
  • Data unrecognizable in Status Column: (Row #${i.loc?.[1] ?? "?"}, "${escapeHtml(i.input)}")
  • `; break; case "malformed": content = `
  • Data malformed in Timestamp Column: (Row #${i.loc?.[1] ?? "?"}, "${escapeHtml(i.input)}")
  • `; break; case "missing": switch (i.loc?.[2]) { case "case id": key = "Case ID"; break; case "timestamp": key = "Timestamp"; break; case "name": key = "Activity"; break; case "instance": key = "Activity Instance ID"; break; case "status": key = "Status"; break; default: key = escapeHtml(String(i.loc?.[2] ?? "")); break; } content = `
  • Data missing in ${key} Column: (Row #${i.loc?.[1] ?? "?"})
  • `; break; } srt += content; }); await Swal.fire({ title: "IMPORT FAILED", html: `

    Error(s) detected:

      ${srt}

    ${manySrt} Please check.

    `, timer: 3000, // Auto-close after 3 seconds showConfirmButton: false, icon: "error", iconColor: "#FF3366", customClass: customClass, }); srt = ""; } /** * Shows a timed success notification after a file upload completes. * @returns {Promise} */ export async function uploadSuccess() { await Swal.fire({ title: "IMPORT COMPLETED", timer: 3000, // Auto-close after 3 seconds showConfirmButton: false, icon: "success", iconColor: "#0099FF", customClass: customClass, }); } /** * Shows a confirmation dialog before uploading a file. Proceeds with * the upload only if the user confirms. * * @param {Object} fetchData - The upload request payload for the backend. * @returns {Promise} */ export async function uploadConfirm(fetchData) { const filesStore = useFilesStore(); const result = await Swal.fire({ title: "ARE YOU SURE?", html: "After importing, you won’t be able to modify labels.", icon: "warning", iconColor: "#FF3366", reverseButtons: true, confirmButtonText: "Yes", confirmButtonColor: "#FF3366", showCancelButton: true, cancelButtonText: "No", cancelButtonColor: "#94a3b8", customClass: customClass, }); if (result.isConfirmed) { filesStore.uploadLog(fetchData); } } /** * Shows a non-dismissable loading spinner during file upload. * @returns {Promise} */ export async function uploadloader() { await Swal.fire({ html: '', showConfirmButton: false, allowOutsideClick: false, customClass: customClass, }); } /** * Shows a modal dialog for renaming a file. * * @param {Function} rename - Backend API function to rename the file. * @param {string} type - The file type (e.g. "log", "filter"). * @param {number} id - The file ID. * @param {string} baseName - The current file name. * @returns {Promise} */ export async function renameModal(rename, type, id, baseName) { const fileName = baseName; const { value, isConfirmed } = await Swal.fire({ title: "RENAME", input: "text", inputPlaceholder: "Enter File Name.", inputValue: fileName, inputAttributes: { maxlength: 200, }, icon: "info", iconHtml: 'edit_square', iconColor: "#0099FF", reverseButtons: true, confirmButtonColor: "#0099FF", showCancelButton: true, cancelButtonColor: "#94a3b8", customClass: customClass, didOpen: () => { const confirmButton = Swal.getConfirmButton(); const inputField = Swal.getInput(); inputField.addEventListener("input", function () { if (!inputField.value.trim()) { confirmButton.classList.add("disable-hover"); confirmButton.disabled = true; } else { confirmButton.classList.remove("disable-hover"); confirmButton.disabled = false; } }); }, }); // Rename succeeded if (isConfirmed) await rename(type, id, value); // Clear the field: fileName = '' } /** * Shows a confirmation dialog for deleting a file and its dependents. * * @param {string} files - HTML string listing dependent files to be deleted. * @param {string} type - The file type (e.g. "log", "filter"). * @param {number} id - The file ID. * @param {string} name - The file name. * @returns {Promise} */ export async function deleteFileModal(files, type, id, name) { const filesStore = useFilesStore(); const safeName = escapeHtml(name); const htmlText = files.length === 0 ? `Do you really want to delete ${safeName}?` : `

    Do you really want to delete ${safeName}?

    The following dependent file(s) will also be deleted:

      ${files}
    `; const deleteCustomClass = { ...customClass }; deleteCustomClass.confirmButton = "!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-danger !bg-neutral-10 !border !border-danger"; const result = await Swal.fire({ title: "CONFIRM DELETION", html: htmlText, icon: "warning", iconColor: "#FF3366", reverseButtons: true, confirmButtonColor: "#ffffff", showCancelButton: true, cancelButtonColor: "#FF3366", customClass: deleteCustomClass, didOpen: () => { const confirmButton = Swal.getConfirmButton(); confirmButton.style.border = "1px solid #FF3366"; }, }); if (result.isConfirmed) { filesStore.deleteFile(type, id); } } /** * Shows a timed success notification after file deletion. * @returns {Promise} */ export async function deleteSuccess() { await Swal.fire({ title: "FILE(S) DELETED", timer: 3000, // Auto-close after 3 seconds showConfirmButton: false, icon: "success", iconColor: "#0099FF", customClass: customClass, }); } /** * Shows an informational modal about files deleted by other users, * then records the deletions and refreshes the file list. * * @param {string} files - HTML string listing the deleted files. * @param {Array<{id: number}>} reallyDeleteData - Array of deleted file * objects with their IDs. * @returns {Promise} */ export async function reallyDeleteInformation(files, reallyDeleteData) { const filesStore = useFilesStore(); const deleteCustomClass = { ...customClass }; const htmlText = `

    The following file(s) have been deleted by other user(s):

      ${files}
    `; deleteCustomClass.confirmButton = "!inline-block !rounded-full !text-sm !font-medium !text-center !align-middle !transition-colors !duration-300 !px-5 !py-2 !w-[100px] !h-[40px] !text-primary !bg-neutral-10 !border !border-primary"; deleteCustomClass.cancelButton = null; await Swal.fire({ title: "FILE(S) DELETED BY OTHER USER(S)", html: htmlText, icon: "info", iconColor: "#0099FF", customClass: deleteCustomClass, confirmButtonColor: "#ffffff", didOpen: () => { const confirmButton = Swal.getConfirmButton(); confirmButton.style.border = "1px solid #0099FF"; }, }); await Promise.all( reallyDeleteData.map((file) => filesStore.deletionRecord(file.id)), ); await filesStore.fetchAllFiles(); } /** * Shows a reminder modal when the user leaves the account management * page with unsaved edits. * @returns {Promise} */ export async function leaveAccountManagementToRemind() { const modalStore = useModalStore(); const result = await Swal.fire({ title: "SAVE YOUR EDIT?", icon: "info", showCancelButton: true, didOpen: () => { const confirmButton = Swal.getConfirmButton(); confirmButton.style.border = "1px solid #0099FF"; }, }); if (result.isConfirmed) { return; } else { modalStore.openModal(); } }