mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add toPlainText utility function and update components to use it for improved text handling
This commit is contained in:
@@ -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> = {};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
47
MobileApp/src/utils/text.ts
Normal file
47
MobileApp/src/utils/text.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user