diff --git a/app/components/assets/assets-index/advanced-asset-columns.tsx b/app/components/assets/assets-index/advanced-asset-columns.tsx index 6f643eb6b..608828f9c 100644 --- a/app/components/assets/assets-index/advanced-asset-columns.tsx +++ b/app/components/assets/assets-index/advanced-asset-columns.tsx @@ -50,6 +50,7 @@ import { type AssetIndexLoaderData } from "~/routes/_layout+/assets._index"; import { getStatusClasses, isOneDayEvent } from "~/utils/calendar"; import { formatCurrency } from "~/utils/currency"; import { getCustomFieldDisplayValue } from "~/utils/custom-fields"; +import { cleanMarkdownFormatting } from "~/utils/markdown-cleaner"; import { isLink } from "~/utils/misc"; import type { OrganizationPermissionSettings } from "~/utils/permissions/custody-and-bookings-permissions.validator.client"; import { userHasCustodyViewPermission } from "~/utils/permissions/custody-and-bookings-permissions.validator.client"; @@ -367,33 +368,42 @@ function StatusColumn({ id, status }: { id: string; status: AssetStatus }) { ); } +/** + * Displays a truncated plain-text preview of the asset description and shows + * the full markdown-rendered content inside a tooltip on hover. + */ function DescriptionColumn({ value }: { value: string }) { - const isEmpty = !value || value.trim().length === 0; + const plainPreview = cleanMarkdownFormatting(value ?? ""); + const hasContent = Boolean(value && value.trim().length > 0); + const previewText = plainPreview.length > 0 ? plainPreview : value.trim(); return ( - {isEmpty ? ( + {!hasContent ? ( - ) : value.length > 60 ? ( + ) : (plainPreview || value).length > 60 ? ( - +
Asset description
-

{value}

+
) : ( - {value} + {previewText} )} ); } +/** + * Renders a compact date cell with optional time information. + */ function DateColumn({ value, includeTime = false, diff --git a/app/utils/csv.server.ts b/app/utils/csv.server.ts index fa6118868..36aab05b8 100644 --- a/app/utils/csv.server.ts +++ b/app/utils/csv.server.ts @@ -44,10 +44,8 @@ import { formatCurrency } from "./currency"; import { SERVER_URL } from "./env"; import { isLikeShelfError, ShelfError } from "./error"; import { ALL_SELECTED_KEY } from "./list"; -import { - cleanMarkdownFormatting, - sanitizeNoteContent, -} from "./note-sanitizer.server"; +import { cleanMarkdownFormatting } from "./markdown-cleaner"; +import { sanitizeNoteContent } from "./note-sanitizer.server"; import { resolveTeamMemberName } from "./user"; export type CSVData = [string[], ...string[][]] | []; diff --git a/app/utils/markdown-cleaner.ts b/app/utils/markdown-cleaner.ts new file mode 100644 index 000000000..565665d03 --- /dev/null +++ b/app/utils/markdown-cleaner.ts @@ -0,0 +1,45 @@ +/** + * Cleans markdown formatting from a text string. + * Shared between server and client utilities that need a plain-text representation. + * + * @param text - Text containing markdown to clean + * @param options - Behaviour modifiers + * @returns Plain text with markdown formatting removed + */ +export type CleanMarkdownFormattingOptions = { + preserveLineBreaks?: boolean; +}; + +export const cleanMarkdownFormatting = ( + text: string, + options: CleanMarkdownFormattingOptions = {} +): string => { + const { preserveLineBreaks = false } = options; + + if (!text) return ""; + + let cleaned = text + .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "") // Remove image references + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1") // Replace markdown links with their text + .replace(/`{3}([\s\S]*?)`{3}/g, (_match, codeBlock) => codeBlock) // Remove code fence markers + .replace(/`([^`]+)`/g, "$1") // Remove inline code markers + .replace(/[*_~]+/g, "") // Remove emphasis characters + .replace(/^\s{0,3}#{1,6}\s+/gm, "") // Remove heading markers + .replace(/^\s{0,3}>\s?/gm, "") // Remove blockquote markers + .replace(/\[[^\]]*\]:\s*\S+/g, "") // Remove reference-style link definitions + .replace(/\[[^\]]*\]/g, (match) => match.replace(/\[|\]/g, "")); // Remove remaining brackets + + if (preserveLineBreaks) { + cleaned = cleaned + .replace(/\r/g, "") + .split("\n") + .map((line) => line.trim().replace(/[ \t]{2,}/g, " ")) + .join("\n") + .replace(/\n{3,}/g, "\n\n"); + } else { + cleaned = cleaned.replace(/\r?\n/g, " ").replace(/\s+/g, " "); + } + + return cleaned.trim(); +}; + diff --git a/app/utils/note-sanitizer.server.ts b/app/utils/note-sanitizer.server.ts index c636b76c0..8ead15842 100644 --- a/app/utils/note-sanitizer.server.ts +++ b/app/utils/note-sanitizer.server.ts @@ -1,45 +1,4 @@ -export type CleanMarkdownFormattingOptions = { - preserveLineBreaks?: boolean; -}; - -/** - * Cleans markdown formatting from a text string - * @param text - Text containing markdown to clean - * @param options - Behaviour modifiers - * @returns Plain text with markdown formatting removed - */ -export const cleanMarkdownFormatting = ( - text: string, - options: CleanMarkdownFormattingOptions = {} -): string => { - const { preserveLineBreaks = false } = options; - - if (!text) return ""; - - let cleaned = text - .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "") // Remove image references - .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1") // Replace markdown links with their text - .replace(/`{3}([\s\S]*?)`{3}/g, (_match, codeBlock) => codeBlock) // Remove code fence markers - .replace(/`([^`]+)`/g, "$1") // Remove inline code markers - .replace(/[*_~]+/g, "") // Remove emphasis characters - .replace(/^\s{0,3}#{1,6}\s+/gm, "") // Remove heading markers - .replace(/^\s{0,3}>\s?/gm, "") // Remove blockquote markers - .replace(/\[[^\]]*\]:\s*\S+/g, "") // Remove reference-style link definitions - .replace(/\[[^\]]*\]/g, (match) => match.replace(/\[|\]/g, "")); // Remove remaining brackets - - if (preserveLineBreaks) { - cleaned = cleaned - .replace(/\r/g, "") - .split("\n") - .map((line) => line.trim().replace(/[ \t]{2,}/g, " ")) - .join("\n") - .replace(/\n{3,}/g, "\n\n"); - } else { - cleaned = cleaned.replace(/\r?\n/g, " ").replace(/\s+/g, " "); - } - - return cleaned.trim(); -}; +import { cleanMarkdownFormatting } from "./markdown-cleaner"; const decodeHtmlEntities = (text: string): string => text