diff --git a/content/modules/parsing.js b/content/modules/parsing.js index 8caff99..d1603c2 100644 --- a/content/modules/parsing.js +++ b/content/modules/parsing.js @@ -1,120 +1,194 @@ -// Parsing functions for tickets, tables, and other elements - +/** + * Parsing functions for tickets, tables, and other elements + * + * This module contains functions for parsing and enhancing various TDX (TeamDynamix) UI elements: + * + * Functions: + * - updateHeading(mutation): Updates report/heading elements with highlighting and embeds + * - Applies queue-based highlighting to headings + * - Handles JSON embed data to replace headings with iframes + * + * - parseTicket(mutation): Main function for parsing ticket/task pages + * - Enhances calendar inputs with modern datetime-local inputs and quick date modifiers + * - Updates browser tab title with ticket title + * - Adds queue tag to ticket header with color coding + * - Handles button links for updates and tasks + * - Adjusts feed colors for dark mode compatibility + * + * - parseOtherElements(): Handles miscellaneous UI enhancements + * - Injects styles into shadow DOM elements (search bars, close buttons) + * - Applies custom desktop layout configurations + * - Enables sticky column positioning + * + * - parseTable(element): Parses table structures and extracts data + * - Extracts table headers and row data + * - Creates structured item objects for each row + * - Calls parseItem() for each row to apply enhancements + * + * - parseItem(item): Applies enhancements and styling to individual table row items + * - Colors queue names with configured colors + * - Converts dates to relative time format (e.g., "3 hours ago") + * - Formats detailed dates (due dates, start dates) with calendar format + * - Highlights modified dates based on age + * - Detects and highlights user replies + * - Highlights internal user modifications + * - Applies person highlighting + * - Applies title highlighting + * - Configures links to open in new tabs + */ + +/** + * Updates heading elements with highlighting and embed functionality + * @param {HTMLElement} mutation - The DOM element containing headings to update + */ function updateHeading(mutation) { + // Find all heading elements in the mutation let headings = mutation.querySelectorAll(".tdx-control-bar__title") for (const heading of headings) { - //color the header if queue name is found - let headingTxt = heading.innerText || heading.textContent - - handleHighlight("report", headingTxt, heading) + // Get heading text and apply queue-based highlighting + let headingText = heading.innerText || heading.textContent + handleHighlight("report", headingText, heading) - //try embedding - if (headingTxt.startsWith("{")) { + // Check if heading contains JSON embed data (starts with "{") + // If so, replace the heading with an iframe embed + if (headingText.startsWith("{")) { try { - let embedData = JSON.parse(headingTxt) - - let title = document.createElement("h4") - title.classList = heading.classList - title.innerText = embedData.name + let embedData = JSON.parse(headingText) + + // Create new title element with embed name + let titleElement = document.createElement("h4") + titleElement.classList = heading.classList + titleElement.innerText = embedData.name + + // Find the module body and create iframe embed let moduleBody = heading.parentNode.parentNode.parentNode.querySelector(".tdx-widget") - let embed = document.createElement("iframe") - embed.classList = "customEmbed" - embed.src = embedData.url - heading.replaceWith(title) - moduleBody.replaceWith(embed) + let embedIframe = document.createElement("iframe") + embedIframe.classList = "customEmbed" + embedIframe.src = embedData.url + + // Replace heading with title and module body with iframe + heading.replaceWith(titleElement) + moduleBody.replaceWith(embedIframe) } catch { + // Silently fail if JSON parsing fails } } } } +/** + * Main function for parsing and enhancing ticket/task pages + * @param {HTMLElement|null} mutation - Optional DOM element to parse (if null, parses entire document) + */ function parseTicket(mutation = null) { - let qBlock = document.querySelector("#ctlAttribute2285") - //ticket or task + // Find queue block element (contains queue name) + let queueBlock = document.querySelector("#ctlAttribute2285") + + // Find ticket or task header element let header = document.getElementById("thTicket_spnTitle") || document.querySelector("#upTaskHeader>div>h1") - //find calendar inputs when editing tickets/tasks - let calendars = document.querySelectorAll("#txtStartDate, #txtEndDate") //tasks for now, will add more as found - if (calendars && calendars.length > 0) { + // Find calendar inputs when editing tickets/tasks + // Currently handles start and end dates for tasks + let calendarInputs = document.querySelectorAll("#txtStartDate, #txtEndDate") + if (calendarInputs && calendarInputs.length > 0) { let originalFormat = "M/D/YYYY h:mm A"; let newFormat = "YYYY-MM-DDTHH:mm"; - [...calendars].forEach(calendar => { - let date = window.moment(calendar.value, originalFormat) - - let newCal = document.createElement("input") - newCal.id = `${calendar.id}-new` - newCal.classList = calendar.classList - newCal.type = "datetime-local" - - //date modifier shortcuts - let calBox = document.createElement("div") - calBox.classList = ["calBox"] - let calTxt = document.createElement("span") - if (date && date.isValid()) { - calTxt.textContent = date.format('dddd') + + [...calendarInputs].forEach(calendarInput => { + // Parse existing date value + let dateMoment = window.moment(calendarInput.value, originalFormat) + + // Create new modern datetime-local input + let newCalendarInput = document.createElement("input") + newCalendarInput.id = `${calendarInput.id}-new` + newCalendarInput.classList = calendarInput.classList + newCalendarInput.type = "datetime-local" + + // Create container for date modifier buttons + let calendarButtonContainer = document.createElement("div") + calendarButtonContainer.classList = ["calBox"] + + // Create text element to display day of week and relative time + let calendarText = document.createElement("span") + if (dateMoment && dateMoment.isValid()) { + calendarText.textContent = dateMoment.format('dddd') } + /** + * Updates both the old TDX calendar input and new datetime-local input + * Also updates the calendar text display + */ function updateDate() { - if (!date || !date.isValid()) { + if (!dateMoment || !dateMoment.isValid()) { return } - //update old tdx calendar - let parsedFormat = date.format(originalFormat) - calendar.value = parsedFormat - - //update new calendar - let iso = date.format(newFormat) - newCal.value = iso - if (calTxt) { - calTxt.textContent = date.calendar() + + // Update old TDX calendar input with original format + let parsedFormat = dateMoment.format(originalFormat) + calendarInput.value = parsedFormat + + // Update new datetime-local calendar input with ISO format + let isoFormat = dateMoment.format(newFormat) + newCalendarInput.value = isoFormat + + // Update calendar text with relative time (e.g., "Today at 2:30 PM") + if (calendarText) { + calendarText.textContent = dateMoment.calendar() } } - //convert to original format - newCal.addEventListener("input", event => { + // Listen for changes to the new datetime-local input + // Convert back to original format when user changes the date + newCalendarInput.addEventListener("input", event => { let parsedValue = window.moment(event.target.value, newFormat) if (parsedValue && parsedValue.isValid()) { - date = parsedValue + dateMoment = parsedValue updateDate() } }) - //modify date - function btnModify(btn) { - if (!date || !date.isValid()) { + /** + * Modifies the date based on button configuration + * Handles business day logic (skips weekends, adjusts for after-hours) + * @param {Object} buttonConfig - Button configuration with 'add' or 'set' properties + */ + function modifyDate(buttonConfig) { + if (!dateMoment || !dateMoment.isValid()) { return } - if ('add' in btn) { - date.add(btn.add) - } else if ('set' in btn) { - date.set(btn.set) + // Apply date modification (add time or set specific time) + if ('add' in buttonConfig) { + dateMoment.add(buttonConfig.add) + } else if ('set' in buttonConfig) { + dateMoment.set(buttonConfig.set) } - //check business days - let day = date.isoWeekday() - let hour = date.hour() - let min = date.minute() + // Check if date falls on weekend or after business hours + let dayOfWeek = dateMoment.isoWeekday() // 1=Monday, 7=Sunday + let hour = dateMoment.hour() + let minute = dateMoment.minute() - //if Friday after 5pm - if (day >= 5 && (hour > 17 || (hour === 17 && min > 0))) { - date.set({ + // If Friday after 5pm, move to next Monday at 8am + if (dayOfWeek >= 5 && (hour > 17 || (hour === 17 && minute > 0))) { + dateMoment.set({ h: 8, - d: date.date() + 7, + d: dateMoment.date() + 7, m: 0 }) - } else if (day > 5) { - //if sat/sun - date.set({ - d: date.date() + 7 + } else if (dayOfWeek > 5) { + // If Saturday or Sunday, move to next Monday + dateMoment.set({ + d: dateMoment.date() + 7 }) } updateDate() } - let buttons = [ + // Define quick date modifier buttons + let dateModifierButtons = [ { name: "+1 day", add: { days: 1 } }, { name: "+1 week", add: { weeks: 1 } }, { name: "+1 month", add: { months: 1 } }, @@ -122,28 +196,31 @@ function parseTicket(mutation = null) { { name: "5PM", set: { h: 17, m: 0 } } ] - for (const btn of buttons) { - let button = document.createElement("button") - button.classList = "btn btn-calendar" - button.innerText = btn.name - button.addEventListener("click", (event) => { + // Create and attach date modifier buttons + for (const buttonConfig of dateModifierButtons) { + let buttonElement = document.createElement("button") + buttonElement.classList = "btn btn-calendar" + buttonElement.innerText = buttonConfig.name + buttonElement.addEventListener("click", (event) => { event.preventDefault() - btnModify(btn) + modifyDate(buttonConfig) }) - calBox.appendChild(button) + calendarButtonContainer.appendChild(buttonElement) } + // Initialize the date display updateDate() - //old input still needs to exist for proper format conversion - calendar.style.display = "none" - calendar.parentElement.append(newCal) - calendar.parentElement.append(calTxt) - calendar.parentElement.append(calBox) + // Hide old calendar input (still needed for format conversion) + // Append new elements to the parent + calendarInput.style.display = "none" + calendarInput.parentElement.append(newCalendarInput) + calendarInput.parentElement.append(calendarText) + calendarInput.parentElement.append(calendarButtonContainer) }); } - //change tab title + // Update browser tab title with ticket title if (header) { let ticketTitle = header.childNodes[0].textContent document.title = ticketTitle @@ -151,55 +228,58 @@ function parseTicket(mutation = null) { return } - //add queue tag to ticket title - if (qBlock && header && !document.querySelector(".qBox")) { - let qElement = qBlock.querySelector("span.wrap-text") - if (!qElement) { + // Add queue tag to ticket title with color coding + // Only add if queue block exists, header exists, and queue box doesn't already exist + if (queueBlock && header && !document.querySelector(".qBox")) { + let queueElement = queueBlock.querySelector("span.wrap-text") + if (!queueElement) { return } - let qTxt = (qElement.innerText || qElement.textContent || "").trim() + let queueText = (queueElement.innerText || queueElement.textContent || "").trim() - if (!qTxt) { + if (!queueText) { return } - let newBox = document.createElement("span") - newBox.textContent = qTxt - newBox.classList.add("qBox") - - let qTxtLower = qTxt.toLowerCase() - for (const qK of Object.keys(colorsByQueue)) { - let q = colorsByQueue[qK] - if (qTxtLower === qK) { - newBox.style.color = q.txt - newBox.style.backgroundColor = q.bg + // Create queue tag element + let queueTagBox = document.createElement("span") + queueTagBox.textContent = queueText + queueTagBox.classList.add("qBox") + + // Apply color coding based on queue name + let queueTextLower = queueText.toLowerCase() + for (const queueKey of Object.keys(colorsByQueue)) { + let queueColorConfig = colorsByQueue[queueKey] + if (queueTextLower === queueKey) { + queueTagBox.style.color = queueColorConfig.txt + queueTagBox.style.backgroundColor = queueColorConfig.bg break } } - header.appendChild(newBox) + header.appendChild(queueTagBox) } - //handle button links - updates/tasks for now - //update buttons + // Handle button links - configure to open in new tabs + // Update ticket buttons [...document.querySelectorAll("#btnUpdateTicket, #divUpdateFromActions")].forEach(button => { handleLink("Update", button) }); - //task button + // Add task button [...document.querySelectorAll("#liAddTicketTask>a")].forEach(button => { handleLink("TicketTaskNew", button) }); - //task template button + // Add task template button [...document.querySelectorAll("#divAddTaskTemplate>a")].forEach(button => { handleLink("TicketAddTaskTemplate", button) }); - //color the feed if darkmode + // Adjust feed colors for dark mode compatibility + // Find feed items either from mutation or document var feedItems - if (mutation) { feedItems = mutation } else { @@ -208,90 +288,95 @@ function parseTicket(mutation = null) { if (feedItems) { for (const feedItem of feedItems) { + // Find all elements with inline color styles [...feedItem.querySelectorAll("*[style*='color']")].forEach(feedElement => { - let color = window.tinycolor(feedElement.style.color) - let brightness = color.getBrightness() - - //if color is too dark - if (brightness < 100 && colorScheme == "darkMode") { - var newColor - var isGrey = true - let rgb = color.toRgb() - - if (rgb.r == rgb.b && rgb.r == rgb.g) { - isGrey = true - } else { - isGrey = false - } - - //if color is shade of grey - if (isGrey) { - let flippedColor = window.tinycolor(invertHex(color.spin(180).toHex())) - newColor = flippedColor + let elementColor = window.tinycolor(feedElement.style.color) + let colorBrightness = elementColor.getBrightness() + + // If color is too dark for dark mode, lighten it + if (colorBrightness < 100 && colorScheme == "darkMode") { + var adjustedColor + let rgb = elementColor.toRgb() + + // Check if color is a shade of grey (all RGB values are equal) + let isGreyColor = (rgb.r == rgb.b && rgb.r == rgb.g) + + // Handle grey colors differently - invert and spin hue + if (isGreyColor) { + let invertedColor = window.tinycolor(invertHex(elementColor.spin(180).toHex())) + adjustedColor = invertedColor } else { - let threshold = 50 - let diff = (threshold - brightness) + brightness - newColor = color.brighten(diff) + // For colored text, brighten it + let brightnessThreshold = 50 + let brightnessDifference = (brightnessThreshold - colorBrightness) + colorBrightness + adjustedColor = elementColor.brighten(brightnessDifference) } - feedElement.style.color = newColor.toHexString() + feedElement.style.color = adjustedColor.toHexString() } }); } } } +/** + * Handles miscellaneous UI enhancements for various TDX elements + * - Injects styles into shadow DOM elements + * - Applies custom desktop layouts + * - Enables sticky column positioning + */ function parseOtherElements() { - //inject styles into search bar + // Inject styles into search bar shadow DOM elements [...document.querySelectorAll("tdx-search-bar, .js_sidePanelX")].forEach(searchBar => { - let shadow = searchBar.shadowRoot - if (shadow) { - injectOtherStyles(shadow); - - //check nested shadows - [...shadow.querySelectorAll("tdx-close-x")].forEach(nested => { - let nestedShadow = nested.shadowRoot - if (nestedShadow) { - injectOtherStyles(nestedShadow) + let shadowRoot = searchBar.shadowRoot + if (shadowRoot) { + injectOtherStyles(shadowRoot); + + // Check for nested shadow DOM elements (e.g., close buttons) + [...shadowRoot.querySelectorAll("tdx-close-x")].forEach(nestedElement => { + let nestedShadowRoot = nestedElement.shadowRoot + if (nestedShadowRoot) { + injectOtherStyles(nestedShadowRoot) } }) } }) + // Apply custom desktop layout configuration let desktopLayout = settings('get', 'layout') - let desktop = document.querySelector("#divContent") - if (desktop && desktopLayout) { - let col1 = desktop.querySelector("#Column1") - let col2 = desktop.querySelector("#Column2") - let col3 = desktop.querySelector("#Column3") - - //100% / 33-66% - if (desktopLayout === "1_100-66-33" && col2 && col3) { - //define width classes - let col2WidthClass = "col-md-8" - let col3WidthClass = "col-md-4" - - //resize column 2 - for (const cls of col2.classList) { - if (cls.startsWith("col-md")) { - col2.classList.remove(cls) - col2.classList.add(col2WidthClass) + let desktopContainer = document.querySelector("#divContent") + if (desktopContainer && desktopLayout) { + let column1 = desktopContainer.querySelector("#Column1") + let column2 = desktopContainer.querySelector("#Column2") + let column3 = desktopContainer.querySelector("#Column3") + + // Apply 100% / 66-33% layout (column 2 takes 66%, column 3 takes 33%) + if (desktopLayout === "1_100-66-33" && column2 && column3) { + // Define width classes for Bootstrap grid + let column2WidthClass = "col-md-8" // 66% width + let column3WidthClass = "col-md-4" // 33% width + + // Resize column 2: remove existing col-md class and add new one + for (const className of column2.classList) { + if (className.startsWith("col-md")) { + column2.classList.remove(className) + column2.classList.add(column2WidthClass) break } } - //resize column 3 - for (const cls of col3.classList) { - if (cls.startsWith("col-md")) { - col3.classList.remove(cls) - col3.classList.add(col3WidthClass) + // Resize column 3: remove existing col-md class and add new one + for (const className of column3.classList) { + if (className.startsWith("col-md")) { + column3.classList.remove(className) + column3.classList.add(column3WidthClass) break } } } } - //apply sticky columns + // Apply sticky column positioning if enabled let stickyColumns = settings('get', 'stickyColumns') if (stickyColumns) { [...document.querySelectorAll(".tdx-dashboard__column")].forEach(column => { @@ -302,6 +387,11 @@ function parseOtherElements() { } } +/** + * Parses a table element and extracts structured data from rows + * Creates item objects for each row and applies enhancements via parseItem() + * @param {HTMLElement} element - The container element containing the table + */ function parseTable(element) { let table = element.querySelector("table") let items = []; @@ -309,72 +399,84 @@ function parseTable(element) { if (table) { let headers = []; - //get headers of the tables + // Extract table headers from elements [...table.querySelectorAll("th")].forEach((header) => { - var txt = header.querySelector("span") - if (txt) { - headers.push(txt.textContent) + var headerText = header.querySelector("span") + if (headerText) { + headers.push(headerText.textContent) } }); - //parse rows + // Parse each data row [...table.querySelectorAll("tr:has(td)")].forEach((row) => { let item = {}; - //parse each cell - let i = 0; + + // Parse each cell in the row (excluding cells with input elements) + let columnIndex = 0; [...row.querySelectorAll("td:not(:has(input))")].forEach((cell) => { - let txt = cell.textContent; - txt = txt.trim() + let cellText = cell.textContent.trim() - item[headers[i]] = { txt: txt, cell: cell } - i++ + // Store cell data with header name as key + item[headers[columnIndex]] = { txt: cellText, cell: cell } + columnIndex++ }) + // Store reference to the row element item.row = row items.push(item) - //begin coloring + // Apply enhancements and styling to this row parseItem(item) }) } } -//modify/color the cells +/** + * Applies enhancements and styling to individual table row items + * - Colors queue names + * - Converts dates to relative time format + * - Formats detailed dates + * - Highlights modified dates based on age + * - Detects and highlights user replies + * - Highlights internal user modifications + * - Applies person and title highlighting + * - Configures links to open in new tabs + * @param {Object} item - Item object containing row data with header names as keys + */ function parseItem(item) { - //color queue names + // Color queue names in "CSS Support" column if ('CSS Support' in item && item["CSS Support"]) { - let qCell = item["CSS Support"].cell - let qTxt = (item["CSS Support"].txt || "").toLowerCase().trim() - if (qTxt && qCell && qTxt in colorsByQueue) { - let q = colorsByQueue[qTxt] - createHighlightBubble(qCell, q.bg, q.txt) + let queueCell = item["CSS Support"].cell + let queueText = (item["CSS Support"].txt || "").toLowerCase().trim() + if (queueText && queueCell && queueText in colorsByQueue) { + let queueColorConfig = colorsByQueue[queueText] + createHighlightBubble(queueCell, queueColorConfig.bg, queueColorConfig.txt) } } - //change date modified - let relativeDates = ['Modified', 'Created'] - for (const dType of relativeDates) { - if (dType in item) { - let dTxt = item[dType].txt - let dCell = item[dType].cell + // Convert dates to relative time format (e.g., "3 hours ago", "in 2 days") + let relativeDateColumns = ['Modified', 'Created'] + for (const dateColumnName of relativeDateColumns) { + if (dateColumnName in item) { + let dateText = item[dateColumnName].txt + let dateCell = item[dateColumnName].cell - if (!dTxt || !dCell) { + if (!dateText || !dateCell) { continue } // Skip if already a relative time string (e.g., "3 hours ago", "in 2 days", "a few seconds ago") // This prevents trying to parse already-converted relative times - // Check for common relative time indicators - let txtTrimmed = dTxt.trim().toLowerCase() - let isRelativeTime = txtTrimmed.endsWith("ago") || - txtTrimmed.includes("from now") || - txtTrimmed.match(/^(a |an |in )?(few |\d+)\s*(second|minute|hour|day|week|month|year)s?\s*(ago|from now)$/i) + let textTrimmed = dateText.trim().toLowerCase() + let isRelativeTime = textTrimmed.endsWith("ago") || + textTrimmed.includes("from now") || + textTrimmed.match(/^(a |an |in )?(few |\d+)\s*(second|minute|hour|day|week|month|year)s?\s*(ago|from now)$/i) if (isRelativeTime) { continue } - // Try parsing with common formats first - let date = window.moment(dTxt, [ + // Try parsing with common date formats first (strict mode) + let dateMoment = window.moment(dateText, [ "ddd M/D/YY h:mm A", "ddd MM/DD/YYYY h:mm A", "M/D/YY h:mm A", @@ -384,31 +486,33 @@ function parseItem(item) { ], true) // Only try loose parsing if strict parsing failed and it doesn't look like a relative time - if (!date.isValid()) { + if (!dateMoment.isValid()) { // Check if it looks like a date format before trying loose parsing // Skip if it's clearly not a date (e.g., relative time strings) - if (!relativeTimePattern.test(dTxt.trim())) { - date = window.moment(dTxt) + if (!relativeTimePattern.test(dateText.trim())) { + dateMoment = window.moment(dateText) } } - if (date.isValid()) { - let dTxtNew = date.fromNow() - if (dTxtNew && dTxtNew !== "Invalid date") { - dCell.textContent = dTxtNew + // Convert to relative time format and update cell + if (dateMoment.isValid()) { + let relativeTimeText = dateMoment.fromNow() + if (relativeTimeText && relativeTimeText !== "Invalid date") { + dateCell.textContent = relativeTimeText } } } } - let detailedDates = ['Date Due', 'Due', 'Created', 'Start'] - for (const dType of detailedDates) { - if (dType in item) { - let dTxt = item[dType].txt - let dCell = item[dType].cell + // Format detailed dates with calendar format (e.g., "Today at 2:30 PM", "Tomorrow at 9:00 AM") + let detailedDateColumns = ['Date Due', 'Due', 'Created', 'Start'] + for (const dateColumnName of detailedDateColumns) { + if (dateColumnName in item) { + let dateText = item[dateColumnName].txt + let dateCell = item[dateColumnName].cell - // Try parsing with common formats first - let date = window.moment(dTxt, [ + // Try parsing with common date formats first (strict mode) + let dateMoment = window.moment(dateText, [ "ddd M/D/YY h:mm A", "ddd MM/DD/YYYY h:mm A", "M/D/YY h:mm A", @@ -417,42 +521,44 @@ function parseItem(item) { "YYYY-MM-DDTHH:mm:ss" ], true) - // If parsing fails, try without strict mode - if (!date.isValid()) { - date = window.moment(dTxt) + // If strict parsing fails, try without strict mode + if (!dateMoment.isValid()) { + dateMoment = window.moment(dateText) } - if (date.isValid()) { - let dTxtNew = date.calendar() - if (dTxtNew && dTxtNew !== "Invalid date") { - dCell.textContent = dTxtNew + // Convert to calendar format and update cell + if (dateMoment.isValid()) { + let calendarFormatText = dateMoment.calendar() + if (calendarFormatText && calendarFormatText !== "Invalid date") { + dateCell.textContent = calendarFormatText } } } } - //highlight modified/age red - let modifiedDates = ['Modified'] - for (const dType of modifiedDates) { - if (dType in item) { - let modDate = item[dType] - if (!modDate || !modDate.txt || !modDate.cell) { + // Highlight modified dates based on age (older = more red/intense) + let modifiedDateColumns = ['Modified'] + for (const dateColumnName of modifiedDateColumns) { + if (dateColumnName in item) { + let modifiedDateData = item[dateColumnName] + if (!modifiedDateData || !modifiedDateData.txt || !modifiedDateData.cell) { continue } - const ageThreshold = 336 + // Age threshold in hours (336 hours = 14 days) + const ageThresholdHours = 336 // Skip if already a relative time string - we can't calculate age from it - let txtTrimmed = modDate.txt.trim().toLowerCase() - let isRelativeTime = txtTrimmed.endsWith("ago") || - txtTrimmed.includes("from now") || - txtTrimmed.match(/^(a |an |in )?(few |\d+)\s*(second|minute|hour|day|week|month|year)s?\s*(ago|from now)$/i) + let textTrimmed = modifiedDateData.txt.trim().toLowerCase() + let isRelativeTime = textTrimmed.endsWith("ago") || + textTrimmed.includes("from now") || + textTrimmed.match(/^(a |an |in )?(few |\d+)\s*(second|minute|hour|day|week|month|year)s?\s*(ago|from now)$/i) if (isRelativeTime) { continue } - // Try parsing with common formats first - let date = window.moment(modDate.txt, [ + // Try parsing with common date formats first (strict mode) + let dateMoment = window.moment(modifiedDateData.txt, [ "ddd M/D/YY h:mm A", "ddd MM/DD/YYYY h:mm A", "M/D/YY h:mm A", @@ -462,67 +568,75 @@ function parseItem(item) { ], true) // Only try loose parsing if strict parsing failed and it doesn't look like a relative time - if (!date.isValid()) { + if (!dateMoment.isValid()) { if (!isRelativeTime) { - date = window.moment(modDate.txt) + dateMoment = window.moment(modifiedDateData.txt) } } // Only highlight if we have a valid date - if (!date.isValid()) { + if (!dateMoment.isValid()) { continue } - let duration = window.moment.duration(window.moment().diff(date)) - let hours = duration.asHours() - - let alpha = hours / ageThreshold - alpha = alpha > 1 ? 1 : alpha - - let cell = modDate.cell - handleHighlight("dateModified", alpha, cell) - cell.classList.add(alpha > 0.5 ? "light" : "dark") + // Calculate age in hours + let duration = window.moment.duration(window.moment().diff(dateMoment)) + let ageInHours = duration.asHours() + + // Calculate alpha value (0-1) based on age, capped at 1 + let ageAlpha = ageInHours / ageThresholdHours + ageAlpha = ageAlpha > 1 ? 1 : ageAlpha + + // Apply highlighting based on age (higher alpha = older = more intense) + let dateCell = modifiedDateData.cell + handleHighlight("dateModified", ageAlpha, dateCell) + // Add class for text color contrast (light text for dark backgrounds, dark for light) + dateCell.classList.add(ageAlpha > 0.5 ? "light" : "dark") } } - //find user replies & last modified by internal + // Detect and highlight user replies and internal modifications if ('Requestor' in item && 'Modified By' in item && 'Prim Resp' in item) { - let fromUser = item["Requestor"] - let lastModified = item["Modified By"] - let assignedTo = item["Prim Resp"] - - //reply from user - if (fromUser.txt === lastModified.txt && fromUser.txt !== assignedTo.txt && assignedTo.txt !== "Unassigned") { + let requestorData = item["Requestor"] + let lastModifiedByData = item["Modified By"] + let assignedToData = item["Prim Resp"] + + // Highlight row if last modified by requestor (user reply) + // Only if assigned to someone else and not unassigned + if (requestorData.txt === lastModifiedByData.txt && + requestorData.txt !== assignedToData.txt && + assignedToData.txt !== "Unassigned") { handleHighlight("reply", null, item.row) } - //modified by internal - if (lastModified.txt !== fromUser.txt && lastModified.txt !== assignedTo.txt) { - let cell = lastModified.cell - handleHighlight("userModified", null, cell) + // Highlight "Modified By" cell if modified by internal user (not requestor or assignee) + if (lastModifiedByData.txt !== requestorData.txt && + lastModifiedByData.txt !== assignedToData.txt) { + let modifiedByCell = lastModifiedByData.cell + handleHighlight("userModified", null, modifiedByCell) } } - //find internal users and highlight them + // Highlight internal users (primary responsibility or responsibility column) if ('Prim Resp' in item) { handleHighlight("person", item['Prim Resp'].txt, item['Prim Resp'].cell) } else if ('Responsibility' in item) { handleHighlight("person", item.Responsibility.txt, item.Responsibility.cell) } - //find inline highlights for tasks + // Apply inline highlighting for task titles if ('Title' in item) { - let title = item.Title - handleHighlight("title", title.txt, title.cell) + let titleData = item.Title + handleHighlight("title", titleData.txt, titleData.cell) } - //find links & open in new tab, if configured - let linkTypes = ['Title', 'TicketID', 'TicketTitle'] - for (const linkType of linkTypes) { - if (linkType in item) { - let link = item[linkType].cell.querySelector("a") - if (link) { - handleLink("report", link) + // Configure links to open in new tabs (if configured) + let linkColumnTypes = ['Title', 'TicketID', 'TicketTitle'] + for (const linkColumnType of linkColumnTypes) { + if (linkColumnType in item) { + let linkElement = item[linkColumnType].cell.querySelector("a") + if (linkElement) { + handleLink("report", linkElement) } } }