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)