profoundjs
Version:
Profound.js Framework and Server
790 lines (711 loc) • 29.3 kB
JavaScript
/* eslint-disable no-var */
/* eslint-disable no-undef */
;
// Description: Fetch and display a list of batch groups.
// When a batch group is clicked, its details are displayed in the right column.
// Querystring parameters:
// - batchid: The ID of the batch group to select and display details for. If not provided,
// the first batch group is selected.
document.addEventListener("DOMContentLoaded", () => {
if (!sessionStorage["atrium-token"]) {
// Redirect to login page if not authenticated, preserving current URL as a 'redirect' parameter
const currentUrl = window.location.pathname + window.location.search;
window.location.href = "/auto-testing/login?redirect=" + encodeURIComponent(currentUrl);
return;
}
// Grab all the elements we need
const leftColumn = document.getElementById("left-column");
// Set default width to 400px
leftColumn.style.flex = "0 0 400px";
const pageTitle = document.getElementById("pageTitle");
const itemDetails = document.getElementById("item-details");
const refreshButton = document.getElementById("refresh-button");
const resizer = document.getElementById("resizer");
let isResizing = false;
// Disable text selection globally during drag
document.addEventListener("selectstart", (e) => {
if (isResizing) e.preventDefault();
});
// Resizer functionality
resizer.addEventListener("mousedown", (e) => {
isResizing = true;
document.body.style.cursor = "ew-resize";
document.addEventListener("mousemove", resizeColumns);
document.addEventListener("mouseup", stopResizing);
});
function resizeColumns(e) {
if (!isResizing) return;
const containerRect = document.getElementById("resizable-container").getBoundingClientRect();
const leftWidth = e.clientX - containerRect.left;
const rightWidth = containerRect.right - e.clientX;
// Prevent columns from shrinking too much
if (leftWidth > 200 && rightWidth > 200) {
leftColumn.style.flex = `0 0 ${leftWidth}px`;
// Make the right column automatically fill the remaining space
itemDetails.style.flex = "1 1 auto";
}
}
function stopResizing() {
isResizing = false;
document.body.style.cursor = "";
document.removeEventListener("mousemove", resizeColumns);
document.removeEventListener("mouseup", stopResizing);
}
// Show error message to the user in the details pane
function displayErrorMessage(message) {
itemDetails.innerHTML = `<div class="details error">
<p>${message}</p>
</div>`;
}
// Environment dropdown state
let envList = [];
let envLookup = {};
let currentEnv = null;
// Batch records cache - updated with fresh data when batch details are loaded
let batchRecords = [];
function renderEnvDropdown() {
const filterPanel = document.getElementById("filter-panel");
if (!filterPanel || !filterPanel.parentNode) {
return;
}
let container = document.getElementById("env-dropdown-container");
let dropdown = document.getElementById("env-dropdown");
if (!container) {
container = document.createElement("div");
container.id = "env-dropdown-container";
container.style.display = "flex";
container.style.alignItems = "center";
container.style.marginBottom = "10px";
const label = document.createElement("label");
label.textContent = "Environment";
label.htmlFor = "env-dropdown";
label.id = "env-dropdown-label";
label.style.marginRight = "8px";
label.style.marginBottom = "0";
dropdown = document.createElement("select");
dropdown.id = "env-dropdown";
dropdown.style.display = "inline-block";
dropdown.style.width = "auto";
dropdown.style.flex = "none";
dropdown.addEventListener("change", () => {
currentEnv = dropdown.value || null;
fetchBatchIds();
});
container.appendChild(label);
container.appendChild(dropdown);
filterPanel.parentNode.insertBefore(container, filterPanel);
}
dropdown = dropdown || document.getElementById("env-dropdown");
dropdown.innerHTML = "";
if (!envList.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "No environments found";
dropdown.appendChild(option);
dropdown.disabled = true;
currentEnv = null;
return;
}
dropdown.disabled = false;
envList.forEach(env => {
const option = document.createElement("option");
option.value = env.envname;
option.textContent = env.envdesc || env.envname;
dropdown.appendChild(option);
});
const defaultValue = currentEnv && envLookup[currentEnv] ? currentEnv : (envList[0] && envList[0].envname);
dropdown.value = defaultValue || "";
currentEnv = dropdown.value || null;
}
function getSelectedEnv() {
const dropdown = document.getElementById("env-dropdown");
if (dropdown && !dropdown.disabled && dropdown.value) {
return dropdown.value;
}
if (currentEnv) {
return currentEnv;
}
if (envList[0]) {
return envList[0].envname;
}
return null;
}
// Fetch environment list and set up dropdown
function fetchEnvList(callback) {
const headers = {
"Content-Type": "application/json"
};
if (sessionStorage["atrium-token"]) {
headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
}
fetch("/auto-testing/env-list", {
method: "POST",
headers
})
.then(res => res.json())
.then(data => {
envList = Array.isArray(data.data) ? data.data : [];
envLookup = {};
envList.forEach(env => {
envLookup[env.envname] = env.envdesc;
});
currentEnv = data.currentEnv && envLookup[data.currentEnv] ? data.currentEnv : (envList[0] && envList[0].envname) || null;
renderEnvDropdown();
if (callback) callback();
})
.catch(err => {
console.error("Error fetching environment list:", err);
envList = [];
envLookup = {};
currentEnv = null;
renderEnvDropdown();
if (callback) callback();
});
}
// Get batch group info from server
function fetchBatchIds() {
// Insert a loading spinner before fetching data
const itemList = document.getElementById("item-list");
itemList.innerHTML = `<div class="spinner"></div>`;
const headers = {
"Content-Type": "application/json"
};
if (sessionStorage["atrium-token"]) {
headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
}
const payload = {};
const selectedEnv = getSelectedEnv();
if (selectedEnv) {
payload.env = selectedEnv;
}
fetch("/auto-testing/batch-list", {
method: "POST",
headers,
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
renderBatchIds(data.data || []);
})
.catch(err => {
console.error("Error fetching batch groups:", err);
displayErrorMessage("Unable to reach the server. Please check your connection.");
});
}
function renderBatchIds(batchIds) {
// Store batch records for updating with fresh data later
batchRecords = batchIds;
const filterPanel = document.getElementById("filter-panel");
const checkedStatuses = new Set(
Array.from(filterPanel.querySelectorAll("input[type=checkbox]:checked"))
.map(checkbox => checkbox.value.toLowerCase())
);
const runSummaryPanel = document.getElementById("run-summary-panel");
const checkedRunSummaries = runSummaryPanel
? new Set(Array.from(runSummaryPanel.querySelectorAll("input[type=checkbox]:checked"))
.map(cb => cb.value))
: new Set();
// Helper: for a given runSummary, does it match the checked run summary filters?
function runSummaryMatches(runsummary) {
const val = (runsummary || "").toLowerCase();
// Always include batches that are running or not started yet.
if (val === "" || val === "running" || val === "not started") {
return true;
}
if (!checkedRunSummaries.size) {
return true;
}
let match = false;
if (checkedRunSummaries.has("passed")) {
if (val === "passed" || val === "mixed") match = true;
}
if (checkedRunSummaries.has("failed")) {
if (val === "failed" || val === "mixed") match = true;
}
if (checkedRunSummaries.has("warning")) {
if (val === "passed with warnings" || val === "mixed with warnings") match = true;
}
return match;
}
const filteredBatches = batchIds.filter(record => {
const statusKey = (record.batchstatussummary || record.abstatus || "").toLowerCase();
if (statusKey && !checkedStatuses.has(statusKey)) {
return false;
}
return runSummaryMatches(record.runsummary);
});
const itemList = document.getElementById("item-list");
itemList.innerHTML = "";
if (filteredBatches.length === 0) {
itemList.innerHTML = `<div class="empty-state">No batches to display.</div>`;
itemDetails.innerHTML = `<div class="details">
<p>No batches match the current filters.</p>
</div>`;
return;
}
filteredBatches.forEach(record => {
const batchStatus = (record.batchstatussummary || record.abstatus || "").toLowerCase();
let batchIconName = "";
let batchIconColor = "";
if (batchStatus === "finished") {
batchIconName = "check_circle";
batchIconColor = "#388E3C";
}
else if (batchStatus === "errored") {
batchIconName = "error";
batchIconColor = "#D32F2F";
}
else if (batchStatus === "running") {
batchIconName = "clock_loader_40";
batchIconColor = "#1976D2";
}
const runStatus = (record.runsummary || "").toLowerCase();
let runIconName = "";
let runIconColor = "";
if (runStatus === "passed") {
runIconName = "thumb_up";
runIconColor = "#388E3C";
}
else if (runStatus === "passed with warnings") {
runIconName = "thumb_up";
runIconColor = "#FF9800";
}
else if (runStatus === "mixed" || runStatus === "mixed with warnings") {
runIconName = "thumbs_up_down";
runIconColor = "#FF9800";
}
else if (runStatus === "failed") {
runIconName = "thumb_down";
runIconColor = "#D32F2F";
}
const div = document.createElement("div");
div.className = "item";
div.dataset.batchId = record.abid;
div.innerHTML = `<span class="work-icon">\
<i class="material-symbols-outlined" title="Batch status: ${batchStatus}" style="color: ${batchIconColor};">${batchIconName}</i>\
</span>\
<span class="status-icon">\
<i class="material-symbols-outlined" title="Run status: ${runStatus}" style="color: ${runIconColor};">${runIconName}</i>\
</span>\
<span class="batch-text">${record.abid}</span>`;
div.addEventListener("click", () => {
document.querySelectorAll(".item").forEach(i => i.classList.remove("active"));
div.classList.add("active");
const totalRuns = record.abtottests ?? 0;
const processedRuns = record.abprctests ?? 0;
const started = record.abstartts && record.abstartts !== "0001-01-01-00.00.00.000000" ? record.abstartts : "";
const finished = record.abfinishts && record.abfinishts !== "0001-01-01-00.00.00.000000" ? record.abfinishts : "";
const envText = `${record.envdesc} (${record.abenv})`;
const detailsHtml = `<table style="width: 100%; border-collapse: collapse;">
<colgroup>
<col style="width:120px">
<col style="width:250px">
<col style="width:100px">
<col>
<col style="width:100px">
<col style="width:200px">
</colgroup>
<tr>
<td><strong>Batch ID:</strong></td>
<td colspan=3>${record.abid}</td>
<td><strong>Status:</strong></td>
<td style="text-transform:capitalize">${record.abstatus || "Unknown"}</td>
</tr><tr>
<td><strong>Started:</strong></td>
<td>${started}</td>
<td><strong>Finished:</strong></td>
<td>${finished}</td>
<td><strong>Processed:</strong></td>
<td>${processedRuns} out of ${totalRuns}</td>
</tr><tr>
<td><strong>Environment:</strong></td>
<td colspan="5">${envText}</td>
</tr>
<tr style="display:${record.aberror ? "table-row" : "none"}">
<td><strong>Error:</strong></td>
<td colspan="5">${record.aberror}</td>
</tr>
</table>
<hr>
<div id="run-grid-container">
<p>Loading test runs...</p>
</div>`;
itemDetails.innerHTML = detailsHtml;
fetchRunList(record.abid);
});
itemList.appendChild(div);
});
const params = new URLSearchParams(window.location.search);
const targetBatchId = params.get("batchId");
if (targetBatchId) {
const targetItem = Array.from(document.querySelectorAll(".item"))
.find(item => item.dataset.batchId === targetBatchId);
if (targetItem) {
targetItem.click();
targetItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
return;
}
}
document.querySelector(".item")?.click();
}
// Listen for changes to filter checkboxes to refresh the batch list
document.getElementById("filter-panel").addEventListener("change", () => {
fetchBatchIds();
});
// Listen for changes to run summary checkboxes to refresh the batch list
const runSummaryPanel = document.getElementById("run-summary-panel");
if (runSummaryPanel) {
runSummaryPanel.addEventListener("change", () => {
fetchBatchIds();
});
}
// Fetch test runs for the given batch id
function fetchRunList(batchId) {
const payload = { batchId };
const headers = {
"Content-Type": "application/json"
};
if (sessionStorage["atrium-token"]) {
headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
}
fetch("/auto-testing/run-list-by-batch-id", {
method: "POST",
headers,
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
// Update batch info if provided
if (data.batchInfo) {
// Find and update the record in batchRecords cache
const recordIndex = batchRecords.findIndex(r => r.abid === batchId);
if (recordIndex >= 0) {
// Update the cached record with fresh data
Object.assign(batchRecords[recordIndex], {
abstatus: data.batchInfo.abstatus || data.batchInfo.ABSTATUS,
abtottests: data.batchInfo.abtottests || data.batchInfo.ABTOTTESTS,
abprctests: data.batchInfo.abprctests || data.batchInfo.ABPRCTESTS,
abenv: data.batchInfo.abenv || data.batchInfo.ABENV,
aberror: data.batchInfo.aberror || data.batchInfo.ABERROR,
abstartts: data.batchInfo.abstartts || data.batchInfo.ABSTARTTS,
abfinishts: data.batchInfo.abfinishts || data.batchInfo.ABFINISHTS
});
// Update the detail pane with fresh data
const details = document.querySelector(".details");
if (details) {
const record = batchRecords[recordIndex];
const totalRuns = record.abtottests ?? 0;
const processedRuns = record.abprctests ?? 0;
// Update status
const statusP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Status:</strong>"));
if (statusP) {
statusP.innerHTML = `<strong>Status:</strong> ${record.abstatus || "Unknown"}`;
}
// Update total test runs processed
const processedP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Total test runs processed:</strong>"));
if (processedP) {
processedP.innerHTML = `<strong>Total test runs processed:</strong> ${processedRuns} out of ${totalRuns}`;
}
// Update started timestamp
const startedP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Started:</strong>"));
if (startedP) {
startedP.innerHTML = `<strong>Started:</strong> ${record.abstartts || "N/A"}`;
}
// Update finished timestamp
const finishedP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Finished:</strong>"));
if (finishedP) {
finishedP.innerHTML = `<strong>Finished:</strong> ${record.abfinishts || "N/A"}`;
}
// Update or add/remove error details
const errorP = Array.from(details.querySelectorAll("p.error-text")).find(p => p.innerHTML.includes("<strong>Last error:</strong>"));
if (record.aberror) {
if (errorP) {
errorP.innerHTML = `<strong>Last error:</strong> ${record.aberror}`;
}
else {
// Insert error paragraph before the run-grid-container
const container = document.getElementById("run-grid-container");
if (container) {
const newErrorP = document.createElement("p");
newErrorP.className = "error-text";
newErrorP.innerHTML = `<strong>Last error:</strong> ${record.aberror}`;
container.parentNode.insertBefore(newErrorP, container);
}
}
}
else if (errorP) {
// Remove error paragraph if no error
errorP.remove();
}
}
}
}
// Render the test runs grid
const runs = data.runs || data;
// Sort it based on last sorting start
const field = Object.keys(runGridSortState)[0] || "arstart";
const sortDirection = runGridSortState[field] || "asc";
runs.sort((a, b) => {
// Setup sortingValue for startTime
if (!a.sortedStartTime) a.sortedStartTime = !a.arstart || a.arstart === "0001-01-01-00.00.00.000000" ? "9999999999999" : a.arstart;
if (!b.sortedStartTime) b.sortedStartTime = !b.arstart || b.arstart === "0001-01-01-00.00.00.000000" ? "9999999999999" : b.arstart;
const sortField = field === "arstart" ? "sortedStartTime" : field;
const valA = a[sortField] || "";
const valB = b[sortField] || "";
if (sortDirection === "asc") {
return valA > valB ? 1 : (valA < valB ? -1 : 0);
}
else {
return valA < valB ? 1 : (valA > valB ? -1 : 0);
}
});
renderRunGrid(runs, runs.length);
})
.catch(err => {
console.error("Error fetching run list:", err);
const container = document.getElementById("run-grid-container");
if (container) {
container.innerHTML = `<p>Error loading test runs.</p>`;
}
});
}
const runGridSortState = { arstart: "asc" };
const gridFilters = {};
function renderRunGrid(runData, total) {
const container = document.getElementById("run-grid-container");
if (!container) return;
// Build header rows dynamically using runGridSortState for sort indicators.
const fields = [
{ field: "testdesc", label: "Test description" },
{ field: "navtext", label: "Item Description" },
{ field: "arstart", label: "Start time" },
{ field: "artime", label: "Run time" },
{ field: "arappjob", label: "Job" },
{ field: "arstatus", label: "Status" }
];
let headerRow = `<tr>
<th class="action-column"></th>`; // reserved for action icons
fields.forEach(f => {
let indicator = "";
if (runGridSortState[f.field]) {
indicator = runGridSortState[f.field] === "asc" ? " ▲" : " ▼";
}
headerRow += `<th data-field="${f.field}">${f.label}${indicator}</th>`;
});
headerRow += `</tr>`;
// Build filtering row
const filterRow = `<tr>
<th class="action-column"></th>
${fields.map(f => `<th><input type="text" placeholder="Filter" data-field="${f.field}" value="${gridFilters[f.field] || ""}"></th>`).join("")}
</tr>`;
// Insert a colgroup to specify column widths
let html = `<table id="run-grid">
<colgroup>
<col> <!-- Action column -->
<col> <!-- Test description -->
<col> <!-- Navigation -->
<col> <!-- Start time -->
<col> <!-- Run time -->
<col> <!-- Job -->
<col> <!-- Status -->
</colgroup>
<thead>
${headerRow}
${filterRow}
</thead>
<tbody>`;
runData.forEach(run => {
const description = run.testdesc || "N/A";
const navDescription = run.navtext || "N/A";
// Parse the start time into date and time parts
const rawStart = run.arstart || "";
let datePart = rawStart;
let timePart = "";
if (rawStart === "0001-01-01-00.00.00.000000") {
datePart = "--";
timePart = "";
}
else if (rawStart) {
const parts = rawStart.split("-");
if (parts.length >= 4) {
datePart = parts.slice(0, 3).join("-"); // "YYYY-MM-DD"
timePart = parts[3]; // "HH.MM.SS.ssssss"
const timeParts = timePart.split(".");
if (timeParts.length >= 3) {
timePart = `${timeParts[0]}:${timeParts[1]}:${timeParts[2]}` +
(timeParts.length > 3 ? `.${timeParts.slice(3).join(".")}` : "");
}
}
}
let show = true;
for (const field in gridFilters) {
const value = gridFilters[field].toLowerCase();
if (value && !run[field].toLowerCase().includes(value)) {
show = false;
break;
}
}
html += `<tr style="display:${show ? "" : "none"}">
<td class="action-cell">
<i class="material-symbols-outlined work-icon" title="Work with Test" onclick="window.open('/auto-testing/work-with-test?testid=${run.artid}','_blank');">list_alt</i>
<i class="material-symbols-outlined work-icon" title="Work with Test Run" onclick="window.open('/auto-testing/work-with-test?runid=${run.arid}','_blank');">fact_check</i>
<i class="material-symbols-outlined work-icon" title="Replay Test" onclick="window.open('/auto-testing/work-with-test?testid=${run.artid}&action=run&env=${run.arenv}','_blank');">play_circle</i>
</td>
<td>${description}</td>
<td>${navDescription}</td>
<td>${datePart} ${timePart}</td>
<td>${run.artime === "0" ? "--" : run.artime}</td>
<td>${run.arappjob || ""}</td>
<td style="text-transform:capitalize">${run.arstatus || ""}</td>
</tr>`;
});
html += `</tbody></table>`;
// Footer with total test runs; will be updated on filtering
html += `<p id="total-runs-footer">Total test runs: ${total}</p>`;
container.innerHTML = html;
// Update visible count if filtering is active.
function updateVisibleCount() {
const tbody = document.querySelector("#run-grid tbody");
let visibleCount = 0;
tbody.querySelectorAll("tr").forEach(tr => {
if (tr.style.display !== "none") visibleCount++;
});
const footer = document.getElementById("total-runs-footer");
if (visibleCount < runData.length) {
footer.textContent = `Total test runs: ${total} (${visibleCount} currently displayed)`;
}
else {
footer.textContent = `Total test runs: ${total}`;
}
}
// Add event listeners for sorting headers.
document.querySelectorAll("#run-grid thead th[data-field]").forEach(th => {
th.style.cursor = "pointer";
th.addEventListener("click", function() {
const field = th.getAttribute("data-field");
let sortDirection = "asc";
if (runGridSortState[field] === "asc") {
sortDirection = "desc";
}
// Clear sort state for all other columns.
for (const key in runGridSortState) {
if (key !== field) {
delete runGridSortState[key];
}
}
runGridSortState[field] = sortDirection; // update global state
// Clear visual sort attributes from all header cells
document.querySelectorAll("#run-grid thead th[data-field]").forEach(header => {
header.removeAttribute("data-sort");
});
th.setAttribute("data-sort", sortDirection);
runData.sort((a, b) => {
const sortField = field === "arstart" ? "sortedStartTime" : field;
const valA = a[sortField] || "";
const valB = b[sortField] || "";
if (sortDirection === "asc") {
return valA > valB ? 1 : (valA < valB ? -1 : 0);
}
else {
return valA < valB ? 1 : (valA > valB ? -1 : 0);
}
});
renderRunGrid(runData, total);
});
});
// Add event listeners for column filtering and update visible count.
document.querySelectorAll("#run-grid thead input").forEach(input => {
input.addEventListener("input", function() {
// Build an object of filter values keyed by data-field name
document.querySelectorAll("#run-grid thead input").forEach(inp => {
const field = inp.getAttribute("data-field");
gridFilters[field] = inp.value.toLowerCase();
});
const tbody = document.querySelector("#run-grid tbody");
// Iterate over each row and check if it should be displayed
tbody.querySelectorAll("tr").forEach(tr => {
let show = true;
tr.querySelectorAll("td").forEach((td, index) => {
// If this cell contains an icon (the first column), skip filtering.
if (td.querySelector(".work-icon")) return;
// Otherwise, determine its corresponding data-field from the header
const headerFields = document.querySelectorAll("#run-grid thead th[data-field]");
const cellIndex = Array.from(td.parentNode.children).indexOf(td);
const field = headerFields[cellIndex - 1] ? headerFields[cellIndex - 1].getAttribute("data-field") : null;
if (field && gridFilters[field] && !td.textContent.toLowerCase().includes(gridFilters[field])) {
show = false;
}
});
tr.style.display = show ? "" : "none";
});
updateVisibleCount();
});
});
updateVisibleCount();
}
if (window.location.search) {
if (window.location.search.includes("batchId=")) {
leftColumn.style.display = "none";
resizer.style.display = "none";
pageTitle.textContent = "Mass Test Details";
const urlParams = new URLSearchParams(window.location.search);
const batchId = urlParams.get("batchId");
var refresh = function() {
itemDetails.innerHTML = `<div class="details">Loading...</div>`;
const headers = {
"Content-Type": "application/json"
};
if (sessionStorage["atrium-token"]) {
headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
}
fetch("/auto-testing/batch-list", {
method: "POST",
headers,
body: JSON.stringify({ batchId: batchId })
})
.then(res => res.json())
.then(data => {
renderBatchIds(data.data || []);
})
.catch(err => {
console.error("Error fetching batch:", err);
displayErrorMessage("Unable to reach the server. Please check your connection.");
});
};
refresh();
refreshButton.addEventListener("click", () => {
refresh();
});
return;
}
}
refreshButton.addEventListener("click", () => {
fetchBatchIds();
});
// Initial render: fetch env list first, then batch groups
fetchEnvList(() => {
fetchBatchIds();
});
// Handle up/down arrow key navigation for list items
document.addEventListener("keydown", function(e) {
const items = document.querySelectorAll(".item");
if (!items.length) return;
const currentIndex = Array.from(items).findIndex(item => item.classList.contains("active"));
if (e.key === "ArrowDown") {
e.preventDefault();
if (currentIndex < items.length - 1) {
const nextItem = items[currentIndex + 1];
nextItem.click();
nextItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
else if (e.key === "ArrowUp") {
e.preventDefault();
if (currentIndex > 0) {
const prevItem = items[currentIndex - 1];
prevItem.click();
prevItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
});
});