diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 277e9db545d..33b55060a74 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -371,6 +371,91 @@ function IssueDetailLoadingState({ ); } +interface InboxMobileToolbarProps { + backHref: string; + issueId: string | undefined; + issueHidden: boolean; + onArchive: () => void; + archivePending: boolean; + onCopy: () => void; + onProperties: () => void; + onHide: () => void; +} + +function InboxMobileToolbar({ + backHref, + issueId: issueIdProp, + issueHidden, + onArchive, + archivePending, + onCopy, + onProperties, + onHide, +}: InboxMobileToolbarProps) { + const navigate = useNavigate(); + const [menuOpen, setMenuOpen] = useState(false); + + return ( +
+ + +
+ {issueIdProp && !issueHidden && ( + + )} + + + + + + + + + {issueIdProp && ( + + )} + + +
+
+ ); +} + export function IssueDetail() { const { issueId } = useParams<{ issueId: string }>(); const { selectedCompanyId } = useCompany(); @@ -383,7 +468,6 @@ export function IssueDetail() { const { pushToast } = useToast(); const { isMobile } = useSidebar(); const [moreOpen, setMoreOpen] = useState(false); - const [inboxMoreOpen, setInboxMoreOpen] = useState(false); const [copied, setCopied] = useState(false); const [mobilePropsOpen, setMobilePropsOpen] = useState(false); const [detailTab, setDetailTab] = useState("chat"); @@ -1655,94 +1739,63 @@ export function IssueDetail() { setTimeout(() => setCopied(false), 2000); }; - // Gmail-style mobile toolbar when viewing an issue from inbox + // Gmail-style mobile toolbar when viewing an issue from inbox. + // Callbacks are stored in a ref so the effect deps stay stable and + // don't trigger an infinite render loop (useMutation results and + // non-memoized functions change identity every render). + const inboxToolbarCallbacksRef = useRef({ + onArchive: () => { + if (!archiveFromInbox.isPending && issue?.id) archiveFromInbox.mutate(issue.id); + }, + onCopy: () => copyIssueToClipboard(), + onProperties: () => setMobilePropsOpen(true), + onHide: () => { + updateIssue.mutate( + { hiddenAt: new Date().toISOString() }, + { onSuccess: () => navigate("/issues/all") }, + ); + }, + }); + inboxToolbarCallbacksRef.current = { + onArchive: () => { + if (!archiveFromInbox.isPending && issue?.id) archiveFromInbox.mutate(issue.id); + }, + onCopy: () => copyIssueToClipboard(), + onProperties: () => setMobilePropsOpen(true), + onHide: () => { + updateIssue.mutate( + { hiddenAt: new Date().toISOString() }, + { onSuccess: () => navigate("/issues/all") }, + ); + }, + }; + + const backHref = sourceBreadcrumb.href ?? "/inbox"; + const showInboxToolbar = isMobile && isFromInbox; + const archivePending = archiveFromInbox.isPending; + const issueHidden = !!issue?.hiddenAt; + useEffect(() => { - if (!isMobile || !isFromInbox) { + if (!showInboxToolbar) { setMobileToolbar(null); return; } setMobileToolbar( -
- - -
- {issue?.id && !issue.hiddenAt && ( - - )} - - - - - - - - - {issue?.id && ( - - )} - - -
-
, + inboxToolbarCallbacksRef.current.onArchive()} + onCopy={() => inboxToolbarCallbacksRef.current.onCopy()} + onProperties={() => inboxToolbarCallbacksRef.current.onProperties()} + onHide={() => inboxToolbarCallbacksRef.current.onHide()} + />, ); return () => setMobileToolbar(null); - }, [ - isMobile, isFromInbox, issue, sourceBreadcrumb, navigate, archiveFromInbox, - copyIssueToClipboard, updateIssue, setMobileToolbar, inboxMoreOpen, setMobilePropsOpen, - ]); + }, [showInboxToolbar, backHref, issue?.id, issueHidden, archivePending, setMobileToolbar]); const issueChatCoreInitialLoading = (commentsLoading && commentPages === undefined)