feat(mobile): wire review note input to inbox approve/reject
- note-agent-actions API: updateApprovalState now accepts optional reviewNote - inbox-store: approve/reject actions pass reviewNote through to backend - Inbox screen: TextInput for review note above approval cards - Review note cleared after successful approve/reject Verification: mobile typecheck passes.
This commit is contained in:
parent
12d90098eb
commit
f8a4c18f27
@ -114,6 +114,7 @@ export async function updateApprovalState(
|
|||||||
id: string,
|
id: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
state: 'approved' | 'rejected',
|
state: 'approved' | 'rejected',
|
||||||
|
reviewNote?: string,
|
||||||
): Promise<MobileApprovalItem> {
|
): Promise<MobileApprovalItem> {
|
||||||
const updated = await getApiClient().fetch<NoteAgentActionDoc>(
|
const updated = await getApiClient().fetch<NoteAgentActionDoc>(
|
||||||
`/note-agent-actions/${encodeURIComponent(id)}?workspaceId=${encodeURIComponent(workspaceId)}`,
|
`/note-agent-actions/${encodeURIComponent(id)}?workspaceId=${encodeURIComponent(workspaceId)}`,
|
||||||
@ -122,6 +123,7 @@ export async function updateApprovalState(
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
state,
|
state,
|
||||||
reviewedAt: new Date().toISOString(),
|
reviewedAt: new Date().toISOString(),
|
||||||
|
...(reviewNote ? { reviewNote } : {}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||||
import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store';
|
import { useInboxStore, type ActivityItem, type ApprovalItem, type InboxState } from '../../store/inbox-store';
|
||||||
import { colors } from '../../theme';
|
import { colors } from '../../theme';
|
||||||
|
|
||||||
@ -11,6 +11,7 @@ export default function InboxScreen() {
|
|||||||
const approve = useInboxStore((state: InboxState) => state.approve);
|
const approve = useInboxStore((state: InboxState) => state.approve);
|
||||||
const reject = useInboxStore((state: InboxState) => state.reject);
|
const reject = useInboxStore((state: InboxState) => state.reject);
|
||||||
const [pendingApprovalId, setPendingApprovalId] = useState<string | null>(null);
|
const [pendingApprovalId, setPendingApprovalId] = useState<string | null>(null);
|
||||||
|
const [reviewNote, setReviewNote] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void hydrate();
|
void hydrate();
|
||||||
@ -21,6 +22,17 @@ export default function InboxScreen() {
|
|||||||
<Text style={styles.title}>Inbox</Text>
|
<Text style={styles.title}>Inbox</Text>
|
||||||
<Text style={styles.body}>Review simple agent actions and approve or reject them from mobile.</Text>
|
<Text style={styles.body}>Review simple agent actions and approve or reject them from mobile.</Text>
|
||||||
{isLoading ? <Text style={styles.empty}>Loading approvals…</Text> : null}
|
{isLoading ? <Text style={styles.empty}>Loading approvals…</Text> : null}
|
||||||
|
{approvals.length > 0 ? (
|
||||||
|
<TextInput
|
||||||
|
style={styles.reviewNoteInput}
|
||||||
|
placeholder="Optional review note…"
|
||||||
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
value={reviewNote}
|
||||||
|
onChangeText={setReviewNote}
|
||||||
|
multiline
|
||||||
|
numberOfLines={2}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{approvals.map((item: ApprovalItem) => (
|
{approvals.map((item: ApprovalItem) => (
|
||||||
<View key={item.id} style={styles.card}>
|
<View key={item.id} style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>{item.title}</Text>
|
<Text style={styles.cardTitle}>{item.title}</Text>
|
||||||
@ -36,7 +48,9 @@ export default function InboxScreen() {
|
|||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setPendingApprovalId(item.id);
|
setPendingApprovalId(item.id);
|
||||||
try {
|
try {
|
||||||
await approve(item.id);
|
const note = reviewNote.trim() || undefined;
|
||||||
|
await approve(item.id, note);
|
||||||
|
setReviewNote('');
|
||||||
} finally {
|
} finally {
|
||||||
setPendingApprovalId(null);
|
setPendingApprovalId(null);
|
||||||
}
|
}
|
||||||
@ -55,7 +69,9 @@ export default function InboxScreen() {
|
|||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setPendingApprovalId(item.id);
|
setPendingApprovalId(item.id);
|
||||||
try {
|
try {
|
||||||
await reject(item.id);
|
const note = reviewNote.trim() || undefined;
|
||||||
|
await reject(item.id, note);
|
||||||
|
setReviewNote('');
|
||||||
} finally {
|
} finally {
|
||||||
setPendingApprovalId(null);
|
setPendingApprovalId(null);
|
||||||
}
|
}
|
||||||
@ -181,4 +197,15 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
|
reviewNoteInput: {
|
||||||
|
backgroundColor: colors.bgElevated,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.borderDefault,
|
||||||
|
color: colors.textPrimary,
|
||||||
|
padding: 12,
|
||||||
|
fontSize: 14,
|
||||||
|
minHeight: 48,
|
||||||
|
textAlignVertical: 'top' as const,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -28,8 +28,8 @@ export type InboxState = {
|
|||||||
activity: ActivityItem[];
|
activity: ActivityItem[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hydrate: () => Promise<void>;
|
hydrate: () => Promise<void>;
|
||||||
approve: (id: string) => Promise<void>;
|
approve: (id: string, reviewNote?: string) => Promise<void>;
|
||||||
reject: (id: string) => Promise<void>;
|
reject: (id: string, reviewNote?: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toApprovalItem(item: MobileApprovalItem): ApprovalItem {
|
function toApprovalItem(item: MobileApprovalItem): ApprovalItem {
|
||||||
@ -53,26 +53,26 @@ export const useInboxStore = create<InboxState>((set, get) => ({
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async approve(id: string) {
|
async approve(id: string, reviewNote?: string) {
|
||||||
const current = get().approvals.find((item) => item.id === id);
|
const current = get().approvals.find((item) => item.id === id);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await updateApprovalState(id, current.workspaceId, 'approved');
|
const updated = await updateApprovalState(id, current.workspaceId, 'approved', reviewNote);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
approvals: state.approvals.map((item) =>
|
approvals: state.approvals.map((item) =>
|
||||||
item.id === id ? toApprovalItem(updated) : item,
|
item.id === id ? toApprovalItem(updated) : item,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
async reject(id: string) {
|
async reject(id: string, reviewNote?: string) {
|
||||||
const current = get().approvals.find((item) => item.id === id);
|
const current = get().approvals.find((item) => item.id === id);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await updateApprovalState(id, current.workspaceId, 'rejected');
|
const updated = await updateApprovalState(id, current.workspaceId, 'rejected', reviewNote);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
approvals: state.approvals.map((item) =>
|
approvals: state.approvals.map((item) =>
|
||||||
item.id === id ? toApprovalItem(updated) : item,
|
item.id === id ? toApprovalItem(updated) : item,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user