diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7a5eff241e6..989137b8831 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -40,10 +40,9 @@ jobs: with: ref: ${{ inputs.ref || github.ref }} - # Why: the build's plain-Node daemon smoke load resolves node-pty. + # Why no native-runtime: this job only produces JS bundles. Native modules + # are prepared in the consumer shards that actually launch Electron. - uses: ./.github/actions/install-node-dependencies - with: - native-runtime: node # Why: building here avoids parallel builds inside Playwright globalSetup; # paired-browser specs also need the standalone web bundle. diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index ab1791c1eb7..6df9dddce05 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -42,6 +42,7 @@ inline src/relay/git-handler.ts inline src/relay/pty-handler.ts inline src/relay/workspace-space-scan.ts inline src/renderer/src/components/GitLabItemDialog.tsx +inline src/renderer/src/components/JiraIssueWorkspace.tsx inline src/renderer/src/components/LinearItemDrawer.tsx inline src/renderer/src/components/TaskPage.tsx inline src/renderer/src/components/Terminal.tsx diff --git a/config/scripts/pr-e2e-gate-contract.test.mjs b/config/scripts/pr-e2e-gate-contract.test.mjs index 665edd01ec0..6aec44fca87 100644 --- a/config/scripts/pr-e2e-gate-contract.test.mjs +++ b/config/scripts/pr-e2e-gate-contract.test.mjs @@ -165,7 +165,7 @@ describe('PR E2E gate contract', () => { (step) => step.uses === './.github/actions/install-node-dependencies' ) - expect(installFor('build').with['native-runtime']).toBe('node') + expect(installFor('build').with).toBeUndefined() for (const jobName of ['e2e', 'changed-e2e', 'ssh-docker-watcher-isolation']) { expect(installFor(jobName).with['native-runtime'], jobName).toBe('electron') } diff --git a/src/renderer/src/components/JiraIssueWorkspace.tsx b/src/renderer/src/components/JiraIssueWorkspace.tsx index d423fe52799..b1f2078900c 100644 --- a/src/renderer/src/components/JiraIssueWorkspace.tsx +++ b/src/renderer/src/components/JiraIssueWorkspace.tsx @@ -1,13 +1,33 @@ +/* eslint-disable max-lines -- Why: the Jira drawer co-locates preview, + metadata edits, and comments so the task page has one full issue surface. */ /* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: Jira issue hydration, comments, transitions, priorities, and user options are loaded from provider IPC for the selected issue. */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + ArrowRight, + Clipboard, + ExternalLink, + GitBranch, + LoaderCircle, + RefreshCw, + Save, + Send, + X +} from 'lucide-react' import { toast } from 'sonner' import { VisuallyHidden } from 'radix-ui' + +import CommentMarkdown from '@/components/sidebar/CommentMarkdown' +import { JiraIcon } from '@/components/icons/JiraIcon' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' import { getCommentBodySubmitState, hasBoundedCommentBodyText } from '@/lib/comment-body-submit-state' - import { createBrowserUuid } from '@/lib/browser-uuid' import { useAppStore } from '@/store' import { @@ -28,9 +48,7 @@ import type { } from '../../../shared/jira-types' import type { TaskSourceContext } from '../../../shared/task-source-context' import { translate } from '@/i18n/i18n' -import { JiraIssueMetadataBar, JiraIssueWorkspaceHeader } from './jira-issue-workspace-chrome' -import { JiraIssueCommentComposer, JiraIssueWorkspaceContent } from './jira-issue-workspace-content' -import { getJiraIssueWorkspaceActions } from './jira-issue-workspace-actions' +import { formatUiRelativeTimeFromDate } from '@/i18n/relative-time-format' type JiraIssueWorkspaceProps = { issue: JiraIssue | null @@ -39,6 +57,50 @@ type JiraIssueWorkspaceProps = { sourceContext?: TaskSourceContext | null } +function formatRelativeTime(input: string): string { + return formatUiRelativeTimeFromDate(input) +} + +function buildJiraBranchName(issue: JiraIssue): string { + const slug = issue.title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 52) + return `${issue.key.toLowerCase()}${slug ? `-${slug}` : ''}` +} + +function buildJiraPrompt(issue: JiraIssue): string { + return `Complete Jira issue ${issue.key}: ${issue.title}\n\n${issue.url}` +} + +function jiraStatusClass(categoryKey: string): string { + if (categoryKey === 'done') { + return 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-200' + } + if (categoryKey === 'indeterminate') { + return 'border-sky-500/30 bg-sky-500/10 text-sky-700 dark:text-sky-200' + } + return 'border-border/50 bg-muted/40 text-muted-foreground' +} + +async function copyTextToClipboard(text: string, label: string): Promise { + try { + await window.api.ui.writeClipboardText(text) + toast.success( + translate('auto.components.JiraIssueWorkspace.2ff69a3545', '{{value0}} copied', { + value0: label + }) + ) + } catch { + toast.error( + translate('auto.components.JiraIssueWorkspace.6c41a9bcea', 'Failed to copy {{value0}}', { + value0: label.toLowerCase() + }) + ) + } +} + export default function JiraIssueWorkspace({ issue, onUse, @@ -281,10 +343,41 @@ export default function JiraIssueWorkspace({ }, [commentDraft, commentSubmitting, displayed, providerSettings]) const canSubmitComment = hasBoundedCommentBodyText(commentDraft) - const actionItems = useMemo( - () => (displayed ? getJiraIssueWorkspaceActions(displayed) : []), - [displayed] - ) + const actionItems = useMemo(() => { + if (!displayed) { + return [] + } + return [ + { + label: translate('auto.components.JiraIssueWorkspace.69da9a208c', 'Open in Jira'), + icon: ExternalLink, + action: () => window.api.shell.openUrl(displayed.url) + }, + { + label: translate('auto.components.JiraIssueWorkspace.779bb91ee0', 'Copy URL'), + icon: Clipboard, + action: () => void copyTextToClipboard(displayed.url, 'URL') + }, + { + label: translate('auto.components.JiraIssueWorkspace.38839801e8', 'Copy key'), + icon: Clipboard, + action: () => void copyTextToClipboard(displayed.key, 'Key') + }, + { + label: translate( + 'auto.components.JiraIssueWorkspace.80efa101c5', + 'Copy suggested branch name' + ), + icon: GitBranch, + action: () => void copyTextToClipboard(buildJiraBranchName(displayed), 'Branch name') + }, + { + label: translate('auto.components.JiraIssueWorkspace.0cc62bd690', 'Copy prompt'), + icon: Clipboard, + action: () => void copyTextToClipboard(buildJiraPrompt(displayed), 'Prompt') + } + ] + }, [displayed]) return ( !open && onClose()}> @@ -311,46 +404,419 @@ export default function JiraIssueWorkspace({ {displayed ? (
- +
+
+
+
+ {displayed.key} + {displayed.siteName ? {displayed.siteName} : null} + {displayed.project.key} + {formatRelativeTime(displayed.updatedAt)} + {issueLoading ? : null} +
+

+ {displayed.title} +

+
+ + + + + + + {translate('auto.components.JiraIssueWorkspace.7a96985ca0', 'Close')} + + +
+
- +
+ + + + + + {transitions.map((transition) => ( + + ))} + + - void loadComments(displayed, requestIdRef.current)} - onUse={onUse} - actionItems={actionItems} - /> + + + + + + + {priorities.map((priority) => ( + + ))} + + - void handleSubmitComment()} - /> + + + + + + + {users.map((user) => ( + + ))} + + +
+ +
+
+
+
+ +
+ setTitleDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' && !event.nativeEvent.isComposing) { + event.preventDefault() + handleSaveTitle() + } + }} + className="h-8 text-xs" + /> + +
+ +
+ setLabelsDraft(event.target.value)} + placeholder={translate( + 'auto.components.JiraIssueWorkspace.0f3c07a901', + 'backend, bug' + )} + className="h-8 text-xs" + /> + +
+
+
+ +
+
+ + + {displayed.issueType.name} + + + {displayed.project.key} ·{' '} + {displayed.assignee?.displayName ?? + translate('auto.components.JiraIssueWorkspace.0b6b5646ed', 'Unassigned')} + +
+ {displayed.description?.trim() ? ( + + ) : ( +

+ {translate( + 'auto.components.JiraIssueWorkspace.c4889a47e4', + 'No description provided.' + )} +

+ )} +
+ +
+
+
+ + {translate('auto.components.JiraIssueWorkspace.9a980b06b9', 'Comments')} + + {comments.length > 0 ? ( + {comments.length} + ) : null} +
+ {commentsError ? ( + + ) : null} +
+ {commentsError ? ( +
+ {commentsError} +
+ ) : commentsLoading && comments.length === 0 ? ( +
+ +
+ ) : comments.length === 0 ? ( +

+ {translate( + 'auto.components.JiraIssueWorkspace.9178090e26', + 'No comments yet.' + )} +

+ ) : ( +
+ {comments.map((comment) => ( +
+
+ {comment.user?.avatarUrl ? ( + + ) : null} + + {comment.user?.displayName ?? + translate( + 'auto.components.JiraIssueWorkspace.666cfdd835', + 'Unknown' + )} + + + {formatRelativeTime(comment.createdAt)} + +
+
+ {/* Why: Jira comment screenshots need the same preview + affordance without changing compact comment typography. */} + +
+
+ ))} +
+ )} +
+
+ + +
+ +
+
+