diff --git a/apps/console/src/pages/system/ApprovalsInboxPage.tsx b/apps/console/src/pages/system/ApprovalsInboxPage.tsx index f64883489..bddce05b9 100644 --- a/apps/console/src/pages/system/ApprovalsInboxPage.tsx +++ b/apps/console/src/pages/system/ApprovalsInboxPage.tsx @@ -81,7 +81,6 @@ import { useObjectTranslation } from '@object-ui/i18n'; import { CheckCircle2, XCircle, - Undo2, Clock, RefreshCw, AlertCircle, @@ -93,12 +92,8 @@ import { User as UserIcon, ChevronLeft, ChevronRight, - ArrowRightLeft, - BellRing, - HelpCircle, Send, Check, - CornerUpLeft, Paperclip, } from 'lucide-react'; import { @@ -435,18 +430,14 @@ export function ApprovalsInboxPage() { const [rejectTarget, setRejectTarget] = useState(null); const [inlineActing, setInlineActing] = useState(null); - // Thread interactions (reassign / request-info / reply / remind) - const [reassignOpen, setReassignOpen] = useState(false); - const [reassignTo, setReassignTo] = useState(''); - const [requestInfoOpen, setRequestInfoOpen] = useState(false); - const [requestInfoText, setRequestInfoText] = useState(''); - // Send back for revision (ADR-0044) — a flow movement, unlike request-info. - const [sendBackOpen, setSendBackOpen] = useState(false); - const [sendBackText, setSendBackText] = useState(''); - const [resubmitting, setResubmitting] = useState(false); + // Thread reply. The secondary decision levers (reassign / request-info / + // send-back / remind / recall / resubmit) are no longer hand-wired here — + // they ship as the object's server-declared actions and render through + // DeclaredActionsBar (objectui#2678 P2-4 + framework#3300). The rich + // approve/reject composer below stays, because it collects file attachments + // the generic param dialog can't yet. const [reply, setReply] = useState(''); const [threadBusy, setThreadBusy] = useState(false); - const [userOptions, setUserOptions] = useState>([]); // Keyboard row focus const [focusIndex, setFocusIndex] = useState(-1); @@ -667,106 +658,6 @@ export function ApprovalsInboxPage() { void load(); }, [load]); - const doReassign = useCallback(async () => { - if (!selected || !reassignTo.trim()) return; - setThreadBusy(true); - try { - await approvalsApi.reassign(selected.id, { - actor_id: resolveActor(selected), to: reassignTo.trim(), comment: comment.trim() || undefined, - }); - toast.success(tr('reassignSuccess', 'Handed to {{to}}', { to: reassignTo.trim() })); - setReassignOpen(false); - setReassignTo(''); - setComment(''); - await refreshThread(selected.id); - refreshBadge(); - } catch (err: any) { - toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); - } finally { - setThreadBusy(false); - } - }, [selected, reassignTo, comment, resolveActor, refreshThread, refreshBadge, humanizeError, tr]); - - const doRemind = useCallback(async () => { - if (!selected) return; - setThreadBusy(true); - try { - const res = await approvalsApi.remind(selected.id, { actor_id: user?.id }); - toast.success(tr('remindSuccess', 'Reminder sent to {{count}} approver(s)', { count: res.notified })); - await refreshThread(selected.id); - } catch (err: any) { - toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); - } finally { - setThreadBusy(false); - } - }, [selected, user?.id, refreshThread, humanizeError, tr]); - - const doRequestInfo = useCallback(async () => { - if (!selected || !requestInfoText.trim()) return; - setThreadBusy(true); - try { - await approvalsApi.requestInfo(selected.id, { - actor_id: resolveActor(selected), comment: requestInfoText.trim(), - }); - toast.success(tr('requestInfoSent', 'Sent back to the requester for more information')); - setRequestInfoOpen(false); - setRequestInfoText(''); - await refreshThread(selected.id); - } catch (err: any) { - toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); - } finally { - setThreadBusy(false); - } - }, [selected, requestInfoText, resolveActor, refreshThread, humanizeError, tr]); - - /** - * Send back for revision (ADR-0044): finalizes this round as `returned`, - * unlocks the record, and parks the flow until the submitter resubmits. - * Past the node's revision budget the server auto-rejects instead. - */ - const doSendBack = useCallback(async () => { - if (!selected) return; - setThreadBusy(true); - try { - const res = await approvalsApi.sendBack(selected.id, { - actor_id: resolveActor(selected), comment: sendBackText.trim() || undefined, - }); - toast.success(res.autoRejected - ? tr('sendBackAutoRejected', 'Revision limit reached — the request was auto-rejected') - : tr('sendBackSuccess', 'Sent back for revision — the requester can now edit and resubmit')); - setSendBackOpen(false); - setSendBackText(''); - await refreshThread(selected.id); - refreshBadge(); - } catch (err: any) { - toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); - } finally { - setThreadBusy(false); - } - }, [selected, sendBackText, resolveActor, refreshThread, refreshBadge, humanizeError, tr]); - - /** - * Resubmit after rework (ADR-0044, submitter): the flow re-enters the - * approval node and opens the next round's request. - */ - const doResubmit = useCallback(async () => { - if (!selected) return; - setResubmitting(true); - try { - await approvalsApi.resubmit(selected.id, { - actor_id: user?.id, comment: comment.trim() || undefined, - }); - toast.success(tr('resubmitSuccess', 'Resubmitted — a new approval round has opened')); - setComment(''); - await refreshThread(selected.id); - refreshBadge(); - } catch (err: any) { - toast.error(humanizeError(err, tr('actionFailed', 'Action failed'))); - } finally { - setResubmitting(false); - } - }, [selected, comment, user?.id, refreshThread, refreshBadge, humanizeError, tr]); - const doReply = useCallback(async () => { if (!selected || !reply.trim()) return; setThreadBusy(true); @@ -781,19 +672,6 @@ export function ApprovalsInboxPage() { } }, [selected, reply, user?.id, refreshThread, humanizeError, tr]); - /** Lazy user directory for the reassign picker (name + email datalist). */ - const loadUserOptions = useCallback(async () => { - if (userOptions.length) return; - try { - const base = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, ''); - const res = await fetch(`${base}/api/v1/data/sys_user?limit=100`, { credentials: 'include' }); - const j = await res.json(); - setUserOptions(((j.records || []) as any[]) - .filter(u => u.email && u.name && u.id !== user?.id) - .map(u => ({ name: String(u.name), email: String(u.email) }))); - } catch { /* picker degrades to free text */ } - }, [userOptions.length, user?.id]); - const canApproveReject = useMemo(() => { if (!selected || selected.status !== 'pending') return false; const pending = new Set(selected.pending_approvers || []); @@ -1477,88 +1355,6 @@ export function ApprovalsInboxPage() { - {/* Reassign dialog */} - { if (!open) { setReassignOpen(false); setReassignTo(''); } }}> - - - {tr('reassignTitle', 'Hand this approval to someone else?')} - - {tr('reassignBody', 'Your approver slot moves to the person you pick — they are notified and can act immediately.')} - - -
- - setReassignTo(e.target.value)} - placeholder={tr('reassignToPlaceholder', 'Pick a user or type an email / role:')} - className="mt-1" - /> - - {userOptions.map(u => ( - - ))} - -
- - {tr('cancel', 'Cancel')} - void doReassign()}> - {tr('reassignBtn', 'Reassign')} - - -
-
- - {/* Request-info dialog */} - { if (!open) { setRequestInfoOpen(false); setRequestInfoText(''); } }}> - - - {tr('requestInfoTitle', 'Ask the requester for more information?')} - - {tr('requestInfoBody', 'The request stays pending; the requester is notified and can reply on the thread.')} - - -