Add MVP features: edit mode, voice input, Gitea push, service autocomplete
- new.tsx: edit mode via editId param, per-field voice dictation (MIC/STOP), service autocomplete dropdown from DB history - incident/[id].tsx: Push to Gitea action reads SecureStore config + md template - index.tsx: homeView=capture redirects to /new on first mount - _layout.tsx: onboarding gate wired (useSettings + router.replace) - ci: GitHub Actions workflow builds debug APK via expo prebuild + Gradle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
name: Build Android APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'temurin'
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
|
restore-keys: gradle-
|
||||||
|
|
||||||
|
- name: Generate Android native project
|
||||||
|
run: npx expo prebuild --platform android --clean
|
||||||
|
|
||||||
|
- name: Build debug APK
|
||||||
|
working-directory: android
|
||||||
|
run: |
|
||||||
|
chmod +x gradlew
|
||||||
|
./gradlew assembleDebug --no-daemon
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: SheetHappens-debug-${{ github.sha }}
|
||||||
|
path: android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
retention-days: 30
|
||||||
+12
-1
@@ -1,10 +1,21 @@
|
|||||||
import { Stack } from "expo-router";
|
import { useEffect } from "react";
|
||||||
|
import { Stack, router } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
|
const { settings, ready } = useSettings();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ready) return;
|
||||||
|
if (!settings.onboardingDone) {
|
||||||
|
router.replace("/onboarding");
|
||||||
|
}
|
||||||
|
}, [ready, settings.onboardingDone]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1, backgroundColor: Colors.bg }}>
|
<GestureHandlerRootView style={{ flex: 1, backgroundColor: Colors.bg }}>
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { getIncidentById, updateIncident } from "@/lib/db";
|
import { getIncidentById, updateIncident } from "@/lib/db";
|
||||||
import { renderMarkdown } from "@/lib/markdown";
|
import { renderMarkdown } from "@/lib/markdown";
|
||||||
|
import { pushIncidentToGitea } from "@/lib/gitea";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ export default function IncidentDetailScreen() {
|
|||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const [incident, setIncident] = useState<Incident | null>(null);
|
const [incident, setIncident] = useState<Incident | null>(null);
|
||||||
|
const [pushing, setPushing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -56,6 +59,33 @@ export default function IncidentDetailScreen() {
|
|||||||
setIncident({ ...incident, status: "resolved" });
|
setIncident({ ...incident, status: "resolved" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGiteaPush() {
|
||||||
|
if (!incident) return;
|
||||||
|
const [url, token, owner, repo, template] = await Promise.all([
|
||||||
|
SecureStore.getItemAsync("gitea_url"),
|
||||||
|
SecureStore.getItemAsync("gitea_token"),
|
||||||
|
SecureStore.getItemAsync("gitea_owner"),
|
||||||
|
SecureStore.getItemAsync("gitea_repo"),
|
||||||
|
SecureStore.getItemAsync("md_template"),
|
||||||
|
]);
|
||||||
|
if (!url || !token || !owner || !repo) {
|
||||||
|
Alert.alert("Gitea not configured", "Set up Gitea in Settings first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPushing(true);
|
||||||
|
const result = await pushIncidentToGitea(
|
||||||
|
incident,
|
||||||
|
{ url, token, owner, repo },
|
||||||
|
template ?? undefined
|
||||||
|
);
|
||||||
|
setPushing(false);
|
||||||
|
if (result.success) {
|
||||||
|
Alert.alert("Pushed", result.url ? `${result.url}` : "Push successful.");
|
||||||
|
} else {
|
||||||
|
Alert.alert("Push failed", result.error ?? "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
const md = renderMarkdown(incident);
|
const md = renderMarkdown(incident);
|
||||||
@@ -188,6 +218,29 @@ export default function IncidentDetailScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={handleGiteaPush}
|
||||||
|
disabled={pushing}
|
||||||
|
style={{
|
||||||
|
backgroundColor: pushing ? Colors.surface2 : Colors.surface,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: pushing ? Colors.border : Colors.primary,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: pushing ? Colors.textDim : Colors.primary,
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: 15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pushing ? "Pushing…" : "Push to Gitea"}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => router.push({ pathname: "/new", params: { editId: incident.id } })}
|
onPress={() => router.push({ pathname: "/new", params: { editId: incident.id } })}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
+12
-1
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -8,12 +8,23 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { getIncidents } from "@/lib/db";
|
import { getIncidents } from "@/lib/db";
|
||||||
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const [incidents, setIncidents] = useState<Incident[]>([]);
|
const [incidents, setIncidents] = useState<Incident[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { settings, ready } = useSettings();
|
||||||
|
const didRedirect = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ready || didRedirect.current) return;
|
||||||
|
if (settings.homeView === "capture") {
|
||||||
|
didRedirect.current = true;
|
||||||
|
router.push("/new");
|
||||||
|
}
|
||||||
|
}, [ready, settings.homeView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadIncidents();
|
loadIncidents();
|
||||||
|
|||||||
+166
-17
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -9,9 +9,15 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Alert,
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router } from "expo-router";
|
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
import { createIncident } from "@/lib/db";
|
import {
|
||||||
|
createIncident,
|
||||||
|
updateIncident,
|
||||||
|
getIncidentById,
|
||||||
|
getDistinctServices,
|
||||||
|
} from "@/lib/db";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
import { useVoice } from "@/hooks/useVoice";
|
||||||
import type { IncidentDraft } from "@/types/incident";
|
import type { IncidentDraft } from "@/types/incident";
|
||||||
|
|
||||||
const FIELD_STYLE = {
|
const FIELD_STYLE = {
|
||||||
@@ -38,10 +44,14 @@ const LABEL_STYLE = {
|
|||||||
fontWeight: "600" as const,
|
fontWeight: "600" as const,
|
||||||
textTransform: "uppercase" as const,
|
textTransform: "uppercase" as const,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
marginBottom: 6,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
type VoiceField = "title" | "symptom" | "rootCause" | "fix";
|
||||||
|
|
||||||
export default function NewIncidentScreen() {
|
export default function NewIncidentScreen() {
|
||||||
|
const { editId } = useLocalSearchParams<{ editId?: string }>();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const [form, setForm] = useState<IncidentDraft>({
|
const [form, setForm] = useState<IncidentDraft>({
|
||||||
title: "",
|
title: "",
|
||||||
service: "",
|
service: "",
|
||||||
@@ -53,6 +63,59 @@ export default function NewIncidentScreen() {
|
|||||||
});
|
});
|
||||||
const [tagInput, setTagInput] = useState("");
|
const [tagInput, setTagInput] = useState("");
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [knownServices, setKnownServices] = useState<string[]>([]);
|
||||||
|
const [serviceFocused, setServiceFocused] = useState(false);
|
||||||
|
|
||||||
|
const { state: voiceState, transcript, start: startVoice, stop: stopVoice } = useVoice();
|
||||||
|
const voiceFieldRef = useRef<VoiceField | null>(null);
|
||||||
|
const [listeningField, setListeningField] = useState<VoiceField | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getDistinctServices().then(setKnownServices);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editId) return;
|
||||||
|
navigation.setOptions({ title: "Edit Incident" });
|
||||||
|
getIncidentById(editId).then((inc) => {
|
||||||
|
if (!inc) return;
|
||||||
|
setForm({
|
||||||
|
title: inc.title,
|
||||||
|
service: inc.service,
|
||||||
|
symptom: inc.symptom,
|
||||||
|
rootCause: inc.rootCause,
|
||||||
|
fix: inc.fix,
|
||||||
|
status: inc.status,
|
||||||
|
tags: inc.tags,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [editId]);
|
||||||
|
|
||||||
|
// Live-fill the active voice field as transcript updates
|
||||||
|
useEffect(() => {
|
||||||
|
const field = voiceFieldRef.current;
|
||||||
|
if (transcript && field) {
|
||||||
|
setForm((f) => ({ ...f, [field]: transcript }));
|
||||||
|
}
|
||||||
|
}, [transcript]);
|
||||||
|
|
||||||
|
// Clear listening state when recognition ends
|
||||||
|
useEffect(() => {
|
||||||
|
if ((voiceState === "idle" || voiceState === "error") && voiceFieldRef.current !== null) {
|
||||||
|
voiceFieldRef.current = null;
|
||||||
|
setListeningField(null);
|
||||||
|
}
|
||||||
|
}, [voiceState]);
|
||||||
|
|
||||||
|
function handleMic(field: VoiceField) {
|
||||||
|
if (listeningField !== null) {
|
||||||
|
stopVoice();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
voiceFieldRef.current = field;
|
||||||
|
setListeningField(field);
|
||||||
|
startVoice("fr-FR");
|
||||||
|
}
|
||||||
|
|
||||||
function set(key: keyof IncidentDraft) {
|
function set(key: keyof IncidentDraft) {
|
||||||
return (value: string) => setForm((f) => ({ ...f, [key]: value }));
|
return (value: string) => setForm((f) => ({ ...f, [key]: value }));
|
||||||
@@ -77,15 +140,71 @@ export default function NewIncidentScreen() {
|
|||||||
}
|
}
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await createIncident(form);
|
if (editId) {
|
||||||
|
await updateIncident(editId, form);
|
||||||
|
} else {
|
||||||
|
await createIncident(form);
|
||||||
|
}
|
||||||
router.back();
|
router.back();
|
||||||
} catch (e) {
|
} catch {
|
||||||
Alert.alert("Error", "Failed to save incident.");
|
Alert.alert("Error", "Failed to save incident.");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredServices =
|
||||||
|
serviceFocused && form.service.length > 0
|
||||||
|
? knownServices.filter(
|
||||||
|
(s) =>
|
||||||
|
s.toLowerCase().includes(form.service.toLowerCase()) &&
|
||||||
|
s !== form.service
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
function micButton(field: VoiceField) {
|
||||||
|
const active = listeningField === field;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => handleMic(field)}
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: active ? "#ef444420" : Colors.surface2,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: active ? Colors.danger : Colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: active ? Colors.danger : Colors.text2,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: "700",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{active ? "STOP" : "MIC"}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelRow(label: string, field: VoiceField) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={LABEL_STYLE}>{label}</Text>
|
||||||
|
{micButton(field)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||||
@@ -95,7 +214,7 @@ export default function NewIncidentScreen() {
|
|||||||
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<Text style={LABEL_STYLE}>Title *</Text>
|
{labelRow("Title *", "title")}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={FIELD_STYLE}
|
style={FIELD_STYLE}
|
||||||
value={form.title}
|
value={form.title}
|
||||||
@@ -105,18 +224,50 @@ export default function NewIncidentScreen() {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={LABEL_STYLE}>Service</Text>
|
<Text style={[LABEL_STYLE, { marginBottom: 6 }]}>Service</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={FIELD_STYLE}
|
style={FIELD_STYLE}
|
||||||
value={form.service}
|
value={form.service}
|
||||||
onChangeText={set("service")}
|
onChangeText={set("service")}
|
||||||
|
onFocus={() => setServiceFocused(true)}
|
||||||
|
onBlur={() => setTimeout(() => setServiceFocused(false), 150)}
|
||||||
placeholder="e.g. ghost-blog, k3s-cluster, haproxy"
|
placeholder="e.g. ghost-blog, k3s-cluster, haproxy"
|
||||||
placeholderTextColor={Colors.textDim}
|
placeholderTextColor={Colors.textDim}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
|
{filteredServices.length > 0 && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: Colors.surface2,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Colors.border,
|
||||||
|
marginTop: -12,
|
||||||
|
marginBottom: 16,
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredServices.slice(0, 5).map((s, i) => (
|
||||||
|
<Pressable
|
||||||
|
key={s}
|
||||||
|
onPress={() => {
|
||||||
|
setForm((f) => ({ ...f, service: s }));
|
||||||
|
setServiceFocused(false);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
padding: 12,
|
||||||
|
borderTopWidth: i === 0 ? 0 : 1,
|
||||||
|
borderTopColor: Colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text1, fontSize: 14 }}>{s}</Text>
|
||||||
|
</Pressable>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={LABEL_STYLE}>Symptom</Text>
|
{labelRow("Symptom", "symptom")}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={MONO_FIELD_STYLE}
|
style={MONO_FIELD_STYLE}
|
||||||
value={form.symptom}
|
value={form.symptom}
|
||||||
@@ -127,7 +278,7 @@ export default function NewIncidentScreen() {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={LABEL_STYLE}>Root Cause</Text>
|
{labelRow("Root Cause", "rootCause")}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={MONO_FIELD_STYLE}
|
style={MONO_FIELD_STYLE}
|
||||||
value={form.rootCause}
|
value={form.rootCause}
|
||||||
@@ -138,7 +289,7 @@ export default function NewIncidentScreen() {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={LABEL_STYLE}>Fix Applied</Text>
|
{labelRow("Fix Applied", "fix")}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={MONO_FIELD_STYLE}
|
style={MONO_FIELD_STYLE}
|
||||||
value={form.fix}
|
value={form.fix}
|
||||||
@@ -149,7 +300,7 @@ export default function NewIncidentScreen() {
|
|||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={LABEL_STYLE}>Tags</Text>
|
<Text style={[LABEL_STYLE, { marginBottom: 6 }]}>Tags</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={FIELD_STYLE}
|
style={FIELD_STYLE}
|
||||||
value={tagInput}
|
value={tagInput}
|
||||||
@@ -181,7 +332,7 @@ export default function NewIncidentScreen() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: Colors.text2, fontSize: 13 }}>{tag}</Text>
|
<Text style={{ color: Colors.text2, fontSize: 13 }}>{tag}</Text>
|
||||||
<Text style={{ color: Colors.textDim, fontSize: 12 }}>✕</Text>
|
<Text style={{ color: Colors.textDim, fontSize: 12 }}>x</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -198,10 +349,8 @@ export default function NewIncidentScreen() {
|
|||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text style={{ color: "#fff", fontSize: 16, fontWeight: "700" }}>
|
||||||
style={{ color: "#fff", fontSize: 16, fontWeight: "700" }}
|
{saving ? "Saving…" : editId ? "Save Changes" : "Save Incident"}
|
||||||
>
|
|
||||||
{saving ? "Saving…" : "Save Incident"}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
Reference in New Issue
Block a user