feat: Add toPlainText utility function and update components to use it for improved text handling

This commit is contained in:
Nawaz Dhandala
2026-02-14 22:02:21 +00:00
parent 66b995c64a
commit d9167b89ba
8 changed files with 88 additions and 21 deletions

View File

@@ -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<string, unknown> = {};

View File

@@ -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}
</Markdown>
);
}

View File

@@ -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({
</View>
{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 (
<View
key={note._id}
key={note._id || `${note.createdAt}-${index}`}
className="rounded-2xl overflow-hidden mb-2.5"
style={{
backgroundColor: theme.colors.backgroundElevated,
@@ -83,7 +87,7 @@ export default function NotesSection({
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{note.note}
{noteText}
</Text>
<View className="flex-row justify-between mt-2.5">
{note.createdByUser ? (
@@ -91,7 +95,7 @@ export default function NotesSection({
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{note.createdByUser.name}
{authorName}
</Text>
) : null}
<Text

View File

@@ -23,6 +23,7 @@ import { changeAlertState } from "../api/alerts";
import { createAlertNote } from "../api/alertNotes";
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";
@@ -183,7 +184,9 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
const currentStateId: string | undefined = alert.currentAlertState?._id;
const isResolved: boolean = resolveState?._id === currentStateId;
const isAcknowledged: boolean = acknowledgeState?._id === currentStateId;
const rootCauseText: string | undefined = alert.rootCause?.trim();
const rootCauseTextRaw: string = toPlainText(alert.rootCause);
const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined;
const descriptionText: string = toPlainText(alert.description);
return (
<ScrollView
@@ -284,7 +287,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
</View>
{/* Description */}
{alert.description ? (
{descriptionText ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<View
@@ -299,7 +302,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{alert.description}
{descriptionText}
</Text>
</View>
</View>

View File

@@ -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 (
<ScrollView
@@ -279,7 +282,7 @@ export default function AlertEpisodeDetailScreen({
</View>
</View>
{episode.description ? (
{descriptionText ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<View
@@ -294,7 +297,7 @@ export default function AlertEpisodeDetailScreen({
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{episode.description}
{descriptionText}
</Text>
</View>
</View>

View File

@@ -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 (
<ScrollView
@@ -298,7 +301,7 @@ export default function IncidentDetailScreen({
</View>
{/* Description */}
{incident.description ? (
{descriptionText ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<View
@@ -313,7 +316,7 @@ export default function IncidentDetailScreen({
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{incident.description}
{descriptionText}
</Text>
</View>
</View>

View File

@@ -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 (
<ScrollView
@@ -288,7 +291,7 @@ export default function IncidentEpisodeDetailScreen({
</View>
</View>
{episode.description ? (
{descriptionText ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<View
@@ -303,7 +306,7 @@ export default function IncidentEpisodeDetailScreen({
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{episode.description}
{descriptionText}
</Text>
</View>
</View>

View File

@@ -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);
}
}