Center plain Project table cells and label the linked PR overflow (#34532)

* fix(github-project): make linked PR numbers clickable

* fix: open linked pull requests at their exact URLs

* test: assert complete project relation cell text

* fix: clarify linked pull request controls and overflow

* Fix linked pull request focus clipping and overflow label

* Keep linked pull request overflow outside clipped values

* fix(github-project): center plain project cells and say what +N means

Drop the clickable linked PR numbers: the numbers stay plain text as
before. Plain text cells (repository, parent issue, linked pull requests,
sub-issue progress) now center vertically like the other cells, and the
linked PR overflow reads "+N more" instead of a bare "+N".

* fix(github-project): keep the linked PR "+N more" visible when numbers truncate
This commit is contained in:
Brennan Benson
2026-10-29 15:04:43 -07:00
committed by GitHub
parent 89d365eb46
commit d2d61ba0c0
5 changed files with 38 additions and 21 deletions
@@ -83,12 +83,14 @@ export default function ProjectCell({
}
if (field.dataType === 'REPOSITORY') {
return (
<span className="truncate text-xs text-muted-foreground">{row.content.repository ?? ''}</span>
<span className="min-w-0 self-center truncate text-xs text-muted-foreground">
{row.content.repository ?? ''}
</span>
)
}
if (field.dataType === 'PARENT_ISSUE') {
return (
<span className="truncate text-xs text-muted-foreground">
<span className="min-w-0 self-center truncate text-xs text-muted-foreground">
{row.content.parentIssue ? `#${row.content.parentIssue.number}` : ''}
</span>
)
@@ -99,14 +101,23 @@ export default function ProjectCell({
return <span className="text-xs text-muted-foreground" />
}
const labels = linked.pullRequests.map((pr) => `#${pr.number}`)
const overflow =
linked.totalCount > linked.pullRequests.length
? ` +${linked.totalCount - linked.pullRequests.length}`
: ''
const overflow = linked.totalCount - linked.pullRequests.length
// Only the numbers truncate, so "+N more" stays visible in narrow columns.
return (
<span className="truncate text-xs text-muted-foreground">
{labels.join(', ')}
{overflow}
<span className="flex min-w-0 items-center gap-1 self-center text-xs text-muted-foreground">
<span className="truncate">{labels.join(', ')}</span>
{overflow > 0 ? (
<>
{' '}
<span className="shrink-0">
{translate(
'auto.components.github.project.ProjectCell.linkedPullRequestsOverflow',
'+{{count}} more',
{ count: overflow }
)}
</span>
</>
) : null}
</span>
)
}
@@ -116,7 +127,7 @@ export default function ProjectCell({
return <span className="text-xs text-muted-foreground" />
}
return (
<span className="truncate text-xs text-muted-foreground">
<span className="min-w-0 self-center truncate text-xs text-muted-foreground">
{progress.completed}/{progress.total} ({progress.percentCompleted}%)
</span>
)
+2 -1
View File
@@ -3364,7 +3364,8 @@
"9cb1a0c984": "Add text",
"2e26a06c70": "Add label",
"36341ffc66": "Assign",
"e369bf4fec": "Select"
"e369bf4fec": "Select",
"linkedPullRequestsOverflow": "+{{count}} more"
},
"ProjectGroupHeader": {
"82a22d2079": "Current",
@@ -128,17 +128,17 @@ describe('project field writes', () => {
.getState()
.updateProjectFieldValue(key, 'row', 'status', { kind: 'single-select', optionId: 'done' })
expect(field(store)).toMatchObject({ optionId: 'done', name: 'Done', color: 'PURPLE' })
expect(visibleRelations()).toEqual(['#12 +2', '1/2 (50%)'])
expect(visibleRelations()).toEqual(['#12 +2 more', '1/2 (50%)'])
const clear = store.getState().clearProjectFieldValue(key, 'row', 'status')
expect(field(store)).toBeUndefined()
expect(visibleRelations()).toEqual(['#12 +2', '1/2 (50%)'])
expect(visibleRelations()).toEqual(['#12 +2 more', '1/2 (50%)'])
await vi.waitFor(() => expect(mockApi.gh.updateProjectItemField).toHaveBeenCalledTimes(1))
expect(mockApi.gh.clearProjectItemField).not.toHaveBeenCalled()
first.resolve({ ok: true })
await Promise.all([update, clear])
expect(mockApi.gh.clearProjectItemField).toHaveBeenCalledTimes(1)
expect(field(store)).toBeUndefined()
expect(visibleRelations()).toEqual(['#12 +2', '1/2 (50%)'])
expect(visibleRelations()).toEqual(['#12 +2 more', '1/2 (50%)'])
})
it.each([true, false])(
@@ -101,7 +101,7 @@ describe('createGitHubSlice.fetchProjectViewTable coordination', () => {
expect(groups.map((group) => group.label)).toEqual(['1', '2'])
expect(groups.flatMap((group) => group.rows.map(projectRelationCellText))).toEqual([
['', ''],
['#12 +2', '1/2 (50%)']
['#12 +2 more', '1/2 (50%)']
])
expect(cached.rows[1].content).not.toHaveProperty('linkedPullRequests')
expect(cached.rows[1].content).not.toHaveProperty('subIssuesProgress')
@@ -70,7 +70,11 @@ const args = {
unsupportedFields: new Set<OptionalProjectItemField>(),
host: 'acme.ghe.com'
} as const
const prText = '#1, #2, #3, #4, #5, #6, #7, #8, #9, #10 +2'
const prText = '#1, #2, #3, #4, #5, #6, #7, #8, #9, #10 +2 more'
// The linked PR overflow renders in its own span, so match the element holding the whole cell text.
const cellText = (text: string) => (_: string, element: Element | null) =>
element?.textContent === text &&
!Array.from(element.children).some((child) => child.textContent === text)
const missingSummary = {
extensions: { code: 'undefinedField', typeName: 'Issue', fieldName: 'subIssuesSummary' },
message: "Field 'subIssuesSummary' doesn't exist on type 'Issue'"
@@ -188,7 +192,7 @@ describe('Project columns from the items response', () => {
}
const row = normalized(page.page.nodes[0])
renderColumns(row)
expect(screen.getByText(expectedText).textContent).toBe(expectedText)
expect(screen.getByText(cellText(expectedText)).textContent).toBe(expectedText)
expect(row.fieldValuesByFieldId).toEqual({
notes: { kind: 'text', fieldId: 'notes', text: 'Keep this too' }
})
@@ -253,7 +257,7 @@ describe('Project columns from the items response', () => {
expect(warning).toHaveBeenCalledTimes(parentDropped ? 1 : 0)
renderColumns(result.rows[0])
expect(Boolean(screen.queryByText('1/2 (50%)'))).toBe(summary)
expect(Boolean(screen.queryByText(prText))).toBe(prs)
expect(Boolean(screen.queryByText(cellText(prText)))).toBe(prs)
expect(ghExecFileAsync).toHaveBeenCalledTimes(2)
const query = vi.mocked(ghExecFileAsync).mock.calls[1][0].join(' ')
expect(query.includes('parent {')).toBe(!parentDropped)
@@ -397,7 +401,8 @@ describe('Project columns from the items response', () => {
row.content.linkedPullRequests.totalCount = totalCount
renderColumns(row)
expect(
screen.getByText(totalCount === 12 ? prText : prText.replace(' +2', '')).textContent
screen.getByText(cellText(totalCount === 12 ? prText : prText.replace(' +2 more', '')))
.textContent
).toBeTruthy()
})
@@ -453,7 +458,7 @@ describe('Project columns from the items response', () => {
const groups = groupRows(table, sorted)
expect(groups.map((group) => group.label)).toEqual(['1', '2'])
renderColumns(groups[1].rows[0])
expect(screen.getByText(prText)).toBeTruthy()
expect(screen.getByText(cellText(prText))).toBeTruthy()
expect(screen.getByText('1/2 (50%)')).toBeTruthy()
cleanup()
renderColumns(groups[0].rows[0])
@@ -519,7 +524,7 @@ describe('Project columns from the items response', () => {
expect(arguments_.join(' ')).toContain('pullRequests(first:10)')
}
renderColumns(result.rows[count - 1])
expect(screen.getByText(prText)).toBeTruthy()
expect(screen.getByText(cellText(prText))).toBeTruthy()
}
)
})