mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
fix(jira): adapt issue list columns to panel width (#28583)
Adapt Jira rows and sort headers to their panel width so titles remain readable in narrow side panels. Refs #16627. Co-authored-by: Leopold Tripot <leopold.tripot@gmail.com>
This commit is contained in:
@@ -126,24 +126,24 @@ function JiraIssueRow({
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'group/row grid min-h-12 cursor-pointer grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-3 py-2 text-left transition hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring md:grid-cols-[90px_minmax(0,1fr)_128px_92px_80px_64px] lg:grid-cols-[96px_minmax(0,1.25fr)_132px_120px_136px_96px_64px] xl:grid-cols-[104px_minmax(0,1.45fr)_144px_132px_160px_128px_72px]',
|
||||
'group/row grid min-h-12 cursor-pointer grid-cols-[minmax(0,1fr)_auto] items-center gap-3 px-3 py-2 text-left transition hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring @3xl:grid-cols-[90px_minmax(0,1fr)_128px_92px_80px_64px] @5xl:grid-cols-[96px_minmax(0,1.25fr)_132px_120px_136px_96px_64px] @7xl:grid-cols-[104px_minmax(0,1.45fr)_144px_132px_160px_128px_72px]',
|
||||
selected && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<span className="block truncate font-mono text-[12px] text-muted-foreground max-md:!hidden">
|
||||
<span className="block truncate font-mono text-[12px] text-muted-foreground @max-3xl:!hidden">
|
||||
{issue.key}
|
||||
</span>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="shrink-0 font-mono text-[11px] text-muted-foreground md:hidden">
|
||||
<span className="shrink-0 font-mono text-[11px] text-muted-foreground @3xl:hidden">
|
||||
{issue.key}
|
||||
</span>
|
||||
<h3 className="min-w-0 truncate text-[13px] font-medium text-foreground">
|
||||
{issue.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1.5 md:!hidden">
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1.5 @3xl:!hidden">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-0 items-center rounded-full border px-1.5 py-0.5 text-[11px] font-medium',
|
||||
@@ -161,8 +161,8 @@ function JiraIssueRow({
|
||||
translate('auto.components.TaskPage.42a9160321', 'Unassigned')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1 max-lg:!hidden">
|
||||
<span className="max-w-[160px] truncate text-[10px] text-muted-foreground xl:!hidden">
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1 @max-5xl:!hidden">
|
||||
<span className="max-w-[160px] truncate text-[10px] text-muted-foreground @7xl:!hidden">
|
||||
{contextLabel}
|
||||
</span>
|
||||
{labels.map((label) => (
|
||||
@@ -181,7 +181,7 @@ function JiraIssueRow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 max-md:!hidden">
|
||||
<div className="flex min-w-0 @max-3xl:!hidden">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex max-w-full items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',
|
||||
@@ -192,11 +192,11 @@ function JiraIssueRow({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="block truncate text-[12px] text-muted-foreground max-md:!hidden">
|
||||
<span className="block truncate text-[12px] text-muted-foreground @max-3xl:!hidden">
|
||||
{issue.priority?.name ?? translate('auto.components.TaskPage.713179dfdc', 'No priority')}
|
||||
</span>
|
||||
|
||||
<div className="flex min-w-0 items-center gap-2 text-[12px] text-muted-foreground max-lg:!hidden">
|
||||
<div className="flex min-w-0 items-center gap-2 text-[12px] text-muted-foreground @max-5xl:!hidden">
|
||||
{issue.assignee?.avatarUrl ? (
|
||||
<img
|
||||
src={issue.assignee.avatarUrl}
|
||||
@@ -216,7 +216,7 @@ function JiraIssueRow({
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="block min-w-0 truncate text-[12px] text-muted-foreground max-md:!hidden">
|
||||
<div className="block min-w-0 truncate text-[12px] text-muted-foreground @max-3xl:!hidden">
|
||||
{formatUpdatedAt(issue.updatedAt)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
@@ -225,7 +225,7 @@ function JiraIssueRow({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-1 md:opacity-0 md:transition-opacity md:group-hover/row:opacity-100 md:group-focus-within/row:opacity-100">
|
||||
<div className="flex shrink-0 items-center justify-end gap-1 @3xl:opacity-0 @3xl:transition-opacity @3xl:group-hover/row:opacity-100 @3xl:group-focus-within/row:opacity-100">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('TaskPage Jira sort controls', () => {
|
||||
'aria-pressed',
|
||||
'true'
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'Assignee' })).toHaveClass('flex', 'max-lg:!hidden')
|
||||
expect(screen.getByRole('button', { name: 'Assignee' })).toHaveClass('flex', '@max-5xl:!hidden')
|
||||
expect(screen.getByRole('button', { name: 'Assignee' })).not.toHaveClass('block')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Key' }))
|
||||
@@ -45,7 +45,7 @@ describe('TaskPage Jira sort controls', () => {
|
||||
const onSort = vi.fn()
|
||||
renderControls(onSort)
|
||||
|
||||
expect(screen.getByTestId('jira-mobile-sort-controls')).toHaveClass('hidden', 'max-md:!flex')
|
||||
expect(screen.getByTestId('jira-mobile-sort-controls')).toHaveClass('hidden', '@max-3xl:!flex')
|
||||
await user.click(screen.getByRole('combobox', { name: 'Sort by' }))
|
||||
await user.click(screen.getByRole('option', { name: 'Priority' }))
|
||||
expect(onSort).toHaveBeenCalledWith('priority')
|
||||
|
||||
@@ -34,7 +34,7 @@ function getJiraSortColumns(): JiraSortColumn[] {
|
||||
{
|
||||
id: 'assignee',
|
||||
label: translate('auto.components.TaskPage.d2a876ca53', 'Assignee'),
|
||||
className: 'max-lg:!hidden'
|
||||
className: '@max-5xl:!hidden'
|
||||
},
|
||||
{ id: 'updated', label: translate('auto.components.TaskPage.f362667d55', 'Updated') }
|
||||
]
|
||||
@@ -63,7 +63,7 @@ export function TaskPageJiraSortControls({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-8 flex-none grid-cols-[90px_minmax(0,1fr)_128px_92px_80px_64px] items-center gap-3 border-b border-border/50 bg-muted/25 px-3 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground max-md:!hidden lg:grid-cols-[96px_minmax(0,1.25fr)_132px_120px_136px_96px_64px] xl:grid-cols-[104px_minmax(0,1.45fr)_144px_132px_160px_128px_72px]">
|
||||
<div className="grid h-8 flex-none grid-cols-[90px_minmax(0,1fr)_128px_92px_80px_64px] items-center gap-3 border-b border-border/50 bg-muted/25 px-3 text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground @max-3xl:!hidden @5xl:grid-cols-[96px_minmax(0,1.25fr)_132px_120px_136px_96px_64px] @7xl:grid-cols-[104px_minmax(0,1.45fr)_144px_132px_160px_128px_72px]">
|
||||
{columns.map((column) => (
|
||||
<button
|
||||
key={column.id}
|
||||
@@ -90,7 +90,7 @@ export function TaskPageJiraSortControls({
|
||||
|
||||
<div
|
||||
data-testid="jira-mobile-sort-controls"
|
||||
className="hidden h-10 flex-none items-center gap-2 border-b border-border/50 bg-muted/25 px-3 max-md:!flex"
|
||||
className="hidden h-10 flex-none items-center gap-2 border-b border-border/50 bg-muted/25 px-3 @max-3xl:!flex"
|
||||
>
|
||||
<span className="shrink-0 text-[11px] font-semibold tracking-[0.05em] text-muted-foreground uppercase">
|
||||
{sortByLabel}
|
||||
|
||||
@@ -70,7 +70,7 @@ export function TaskPageJiraContent({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-0 max-h-full flex-col overflow-hidden rounded-md rounded-t-none border border-t-0 border-border/50 bg-background shadow-sm">
|
||||
<div className="@container flex min-h-0 max-h-full flex-col overflow-hidden rounded-md rounded-t-none border border-t-0 border-border/50 bg-background shadow-sm">
|
||||
<div className="flex h-10 flex-none items-center justify-between gap-3 border-b border-border/50 bg-muted/35 px-3">
|
||||
<div className="min-w-0 text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
||||
{translate('auto.components.TaskPage.63b2abd3aa', 'Jira issues')}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { JiraIssue } from '../../src/shared/jira-types'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
|
||||
const ISSUE_TITLE = 'Keep planning notes visible while switching between project tasks'
|
||||
const ISSUE: JiraIssue = {
|
||||
id: 'layout-fixture',
|
||||
key: 'DEMO-101',
|
||||
siteId: 'layout-site',
|
||||
title: ISSUE_TITLE,
|
||||
url: 'https://example.atlassian.net/browse/DEMO-101',
|
||||
project: { id: 'layout-project', key: 'DEMO', name: 'Demo' },
|
||||
issueType: { id: 'task', name: 'Task' },
|
||||
status: { id: 'open', name: 'To Do', categoryKey: 'new', categoryName: 'To Do' },
|
||||
labels: [],
|
||||
priority: { id: 'high', name: 'High' },
|
||||
assignee: { accountId: 'layout-user', displayName: 'Demo User' },
|
||||
createdAt: '2026-10-01T00:00:00Z',
|
||||
updatedAt: '2026-10-13T00:00:00Z'
|
||||
}
|
||||
|
||||
test('Jira titles and sort headings adapt to panel width inside a wide window', async ({
|
||||
orcaPage
|
||||
}) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await orcaPage.setViewportSize({ width: 1500, height: 900 })
|
||||
await orcaPage.evaluate((issue) => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('Store unavailable')
|
||||
}
|
||||
const state = store.getState()
|
||||
if (!state.settings) {
|
||||
throw new Error('Settings unavailable')
|
||||
}
|
||||
const status = {
|
||||
connected: true,
|
||||
viewer: null,
|
||||
selectedSiteId: 'layout-site',
|
||||
sites: [
|
||||
{
|
||||
id: 'layout-site',
|
||||
siteUrl: 'https://example.atlassian.net',
|
||||
email: '',
|
||||
displayName: 'Demo Jira',
|
||||
accountId: 'layout-user'
|
||||
}
|
||||
]
|
||||
}
|
||||
store.setState({
|
||||
settings: { ...state.settings, defaultTaskSource: 'jira', visibleTaskProviders: ['jira'] },
|
||||
jiraStatus: status,
|
||||
jiraStatusChecked: true,
|
||||
jiraStatusContextKey: 'local#0',
|
||||
checkJiraConnection: async () => {},
|
||||
readJiraStatus: async () => status,
|
||||
listJiraIssues: async () => [issue],
|
||||
searchJiraIssues: async () => [issue]
|
||||
})
|
||||
store.getState().openTaskPage({ taskSource: 'jira' })
|
||||
}, ISSUE)
|
||||
|
||||
const title = orcaPage.getByRole('heading', { name: ISSUE_TITLE })
|
||||
await expect(title).toBeVisible()
|
||||
for (const width of [500, 780, 1050, 1320]) {
|
||||
await title.evaluate((element, width) => {
|
||||
const host =
|
||||
element.closest('[role="button"]')?.parentElement?.parentElement?.parentElement
|
||||
?.parentElement?.parentElement
|
||||
if (!(host instanceof HTMLElement)) {
|
||||
throw new Error('Jira list host unavailable')
|
||||
}
|
||||
host.style.width = `${width}px`
|
||||
host.style.maxWidth = 'none'
|
||||
}, width)
|
||||
await expect
|
||||
.poll(() => title.evaluate((element) => element.getBoundingClientRect().width))
|
||||
.toBeGreaterThan(200)
|
||||
const mobileSort = orcaPage.getByTestId('jira-mobile-sort-controls')
|
||||
const assignee = orcaPage.getByRole('button', { name: 'Assignee', exact: true })
|
||||
await (width < 768 ? expect(mobileSort).toBeVisible() : expect(mobileSort).toBeHidden())
|
||||
await (width >= 1024 ? expect(assignee).toBeVisible() : expect(assignee).toBeHidden())
|
||||
if (width >= 768) {
|
||||
const grids = await title.evaluate((element) => {
|
||||
const row = element.closest('[role="button"]')
|
||||
const host = row?.parentElement?.parentElement?.parentElement?.parentElement?.parentElement
|
||||
const header = host?.children[1]
|
||||
if (!row || !header) {
|
||||
throw new Error('Jira header or row unavailable')
|
||||
}
|
||||
return [
|
||||
getComputedStyle(row).gridTemplateColumns.split(' '),
|
||||
getComputedStyle(header).gridTemplateColumns.split(' ')
|
||||
]
|
||||
})
|
||||
// The scroll gutter narrows the flexible title; fixed tracks must still match.
|
||||
expect(grids[0].filter((_, index) => index !== 1)).toEqual(
|
||||
grids[1].filter((_, index) => index !== 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user