diff --git a/MobileApp/src/api/client.ts b/MobileApp/src/api/client.ts index 57aeef59e8..5f66ec4c9e 100644 --- a/MobileApp/src/api/client.ts +++ b/MobileApp/src/api/client.ts @@ -33,10 +33,12 @@ function normalizeResponseData(data: unknown): unknown { // Check for serialized OneUptime types if ( typeof obj["_type"] === "string" && - typeof obj["value"] === "string" && - (obj["_type"] === "ObjectID" || obj["_type"] === "DateTime") + Object.prototype.hasOwnProperty.call(obj, "value") && + (obj["_type"] === "ObjectID" || + obj["_type"] === "DateTime" || + obj["_type"] === "Markdown") ) { - return obj["value"]; + return normalizeResponseData(obj["value"]); } const normalized: Record = {}; diff --git a/MobileApp/src/components/MarkdownContent.tsx b/MobileApp/src/components/MarkdownContent.tsx index a6f9785d1c..2e35e7adf2 100644 --- a/MobileApp/src/components/MarkdownContent.tsx +++ b/MobileApp/src/components/MarkdownContent.tsx @@ -3,9 +3,10 @@ import { StyleSheet } from "react-native"; import Markdown from "react-native-markdown-display"; import * as Linking from "expo-linking"; import { useTheme } from "../theme"; +import { toPlainText } from "../utils/text"; export interface MarkdownContentProps { - content: string; + content: unknown; variant?: "primary" | "secondary"; } @@ -14,6 +15,7 @@ export default function MarkdownContent({ variant = "primary", }: MarkdownContentProps): React.JSX.Element { const { theme } = useTheme(); + const markdownText: string = toPlainText(content); const isSecondary: boolean = variant === "secondary"; const textColor: string = isSecondary ? theme.colors.textSecondary @@ -91,7 +93,7 @@ export default function MarkdownContent({ return false; }} > - {content} + {markdownText} ); } \ No newline at end of file diff --git a/MobileApp/src/components/NotesSection.tsx b/MobileApp/src/components/NotesSection.tsx index 3c819ccaac..53a9967aea 100644 --- a/MobileApp/src/components/NotesSection.tsx +++ b/MobileApp/src/components/NotesSection.tsx @@ -3,6 +3,7 @@ import { View, Text, TouchableOpacity } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { useTheme } from "../theme"; import { formatDateTime } from "../utils/date"; +import { toPlainText } from "../utils/text"; import type { NoteItem } from "../api/types"; interface NotesSectionProps { @@ -62,10 +63,13 @@ export default function NotesSection({ {notes && notes.length > 0 - ? notes.map((note: NoteItem) => { + ? notes.map((note: NoteItem, index: number) => { + const noteText: string = toPlainText(note.note); + const authorName: string = toPlainText(note.createdByUser?.name); + return ( - {note.note} + {noteText} {note.createdByUser ? ( @@ -91,7 +95,7 @@ export default function NotesSection({ className="text-[12px]" style={{ color: theme.colors.textTertiary }} > - {note.createdByUser.name} + {authorName} ) : null} {/* Description */} - {alert.description ? ( + {descriptionText ? ( - {alert.description} + {descriptionText} diff --git a/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx b/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx index cc8e2407f8..01ffdad602 100644 --- a/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx +++ b/MobileApp/src/screens/AlertEpisodeDetailScreen.tsx @@ -25,6 +25,7 @@ import { } from "../api/alertEpisodes"; import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; +import { toPlainText } from "../utils/text"; import type { AlertsStackParamList } from "../navigation/types"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; import type { AlertState } from "../api/types"; @@ -191,7 +192,9 @@ export default function AlertEpisodeDetailScreen({ const currentStateId: string | undefined = episode.currentAlertState?._id; const isResolved: boolean = resolveState?._id === currentStateId; const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; - const rootCauseText: string | undefined = episode.rootCause?.trim(); + const rootCauseTextRaw: string = toPlainText(episode.rootCause); + const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined; + const descriptionText: string = toPlainText(episode.description); return ( - {episode.description ? ( + {descriptionText ? ( - {episode.description} + {descriptionText} diff --git a/MobileApp/src/screens/IncidentDetailScreen.tsx b/MobileApp/src/screens/IncidentDetailScreen.tsx index 6ffcbeae39..6796b74166 100644 --- a/MobileApp/src/screens/IncidentDetailScreen.tsx +++ b/MobileApp/src/screens/IncidentDetailScreen.tsx @@ -23,6 +23,7 @@ import { changeIncidentState } from "../api/incidents"; import { createIncidentNote } from "../api/incidentNotes"; import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; +import { toPlainText } from "../utils/text"; import type { IncidentsStackParamList } from "../navigation/types"; import { QueryClient, useQueryClient } from "@tanstack/react-query"; import AddNoteModal from "../components/AddNoteModal"; @@ -197,7 +198,9 @@ export default function IncidentDetailScreen({ const currentStateId: string | undefined = incident.currentIncidentState?._id; const isResolved: boolean = resolveState?._id === currentStateId; const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; - const rootCauseText: string | undefined = incident.rootCause?.trim(); + const rootCauseTextRaw: string = toPlainText(incident.rootCause); + const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined; + const descriptionText: string = toPlainText(incident.description); return ( {/* Description */} - {incident.description ? ( + {descriptionText ? ( - {incident.description} + {descriptionText} diff --git a/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx b/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx index d6e9252a82..d094a2163f 100644 --- a/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx +++ b/MobileApp/src/screens/IncidentEpisodeDetailScreen.tsx @@ -25,6 +25,7 @@ import { } from "../api/incidentEpisodes"; import { rgbToHex } from "../utils/color"; import { formatDateTime } from "../utils/date"; +import { toPlainText } from "../utils/text"; import type { IncidentsStackParamList } from "../navigation/types"; import type { IncidentState } from "../api/types"; import { useQueryClient } from "@tanstack/react-query"; @@ -200,7 +201,9 @@ export default function IncidentEpisodeDetailScreen({ const currentStateId: string | undefined = episode.currentIncidentState?._id; const isResolved: boolean = resolveState?._id === currentStateId; const isAcknowledged: boolean = acknowledgeState?._id === currentStateId; - const rootCauseText: string | undefined = episode.rootCause?.trim(); + const rootCauseTextRaw: string = toPlainText(episode.rootCause); + const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined; + const descriptionText: string = toPlainText(episode.description); return ( - {episode.description ? ( + {descriptionText ? ( - {episode.description} + {descriptionText} diff --git a/MobileApp/src/utils/text.ts b/MobileApp/src/utils/text.ts new file mode 100644 index 0000000000..80bab6b008 --- /dev/null +++ b/MobileApp/src/utils/text.ts @@ -0,0 +1,47 @@ +interface SerializedValue { + _type?: unknown; + value?: unknown; +} + +function isSerializedValue(value: unknown): value is SerializedValue { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + + return "_type" in value && "value" in value; +} + +export function toPlainText(value: unknown): string { + if (value === null || value === undefined) { + return ""; + } + + if (typeof value === "string") { + return value; + } + + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + + if (isSerializedValue(value)) { + return toPlainText(value.value); + } + + if (Array.isArray(value)) { + return value + .map((item: unknown) => { + return toPlainText(item); + }) + .filter((item: string) => { + return item.length > 0; + }) + .join(", "); + } + + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}