Files
lucia-frontend/src/module/alertModal.js
2026-03-10 00:31:57 +08:00

640 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<boolean>} 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:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
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<void>}
*/
export async function savedSuccessfully(value = "") {
await Swal.fire({
title: "SAVE COMPLETE",
html: `<span class="text-primary">${escapeHtml(value)}</span> 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<void>}
*/
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<boolean>} 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:
'<span class="material-symbols-outlined !text-[58px]">cloud_upload</span>',
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<void>}
*/
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<Object>} 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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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 = `<li>Data unrecognizable in Status Column: (Row #${i.loc?.[1] ?? "?"}, "${escapeHtml(i.input)}")</li>`;
break;
case "malformed":
content = `<li>Data malformed in Timestamp Column: (Row #${i.loc?.[1] ?? "?"}, "${escapeHtml(i.input)}")</li>`;
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 = `<li>Data missing in ${key} Column: (Row #${i.loc?.[1] ?? "?"})</li>`;
break;
}
srt += content;
});
await Swal.fire({
title: "IMPORT FAILED",
html: `<div class="text-left mx-3 space-y-1"><p>Error(s) detected:</p><ul class="list-disc ml-6">${srt}</ul><p>${manySrt} Please check.</p></div>`,
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<void>}
*/
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<void>}
*/
export async function uploadConfirm(fetchData) {
const filesStore = useFilesStore();
const result = await Swal.fire({
title: "ARE YOU SURE?",
html: "After importing, you wont 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<void>}
*/
export async function uploadloader() {
await Swal.fire({
html: '<span class="loaderBar mt-7"></span>',
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<void>}
*/
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:
'<span class="material-symbols-outlined !text-[58px]">edit_square</span>',
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<void>}
*/
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 <span class="text-primary">${safeName}</span>?`
: `<div class="text-left mx-4 space-y-1"><p class="mb-2">Do you really want to delete <span class="text-primary">${safeName}</span>?</p><p>The following dependent file(s) will also be deleted:</p><ul class="list-disc ml-6">${files}</ul></div>`;
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<void>}
*/
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<void>}
*/
export async function reallyDeleteInformation(files, reallyDeleteData) {
const filesStore = useFilesStore();
const deleteCustomClass = { ...customClass };
const htmlText = `<div class="text-left mx-4 space-y-1"><p>The following file(s) have been deleted by other user(s):</p><ul class="list-disc ml-6">${files}</ul></div>`;
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<void>}
*/
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();
}
}