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