fix(github): name an unfiltered empty project view instead of blaming a filter (#20588)

* fix(github): skip Projects search index for unfiltered views

Empty query still used items(query:\$q), which routes through GitHub's
Projects search index and can return totalCount 0 while the board is full
during index lag. Omit the query argument when the view filter is empty.

Fixes #12648.

* docs(github): drop the false stable-shape claim for empty project filters

Unfiltered item fetches omit items(query:) so boards skip search-index
lag. The View.filter field is still '' when GitHub returns null.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(github): name an unfiltered empty project view instead of blaming a filter

The search-index workaround in this branch was a no-op. Live introspection of
ProjectV2.items shows `query` is declared `String = ""`, so omitting the
argument and sending `$q = ""` coerce to the identical resolver input; GitHub
applies declared defaults for omitted args (verified against its own endpoint).
There is no non-search item field on ProjectV2 and ProjectV2View has no `items`
at all, so no request shape can dodge the index. Revert the branching query
construction and the module it added.

What the user actually reported in #12648 is the copy: a view with no filter
rendered "No items match this view's filter", which reads as data loss when a
freshly populated board momentarily comes back empty. Word the empty state from
the view's own filter — the filter message only when there is a filter, and an
honest "no items yet" plus a transience hint when there is not — and share the
one implementation between the table and roadmap surfaces.

Refs #12648.

---------

Co-authored-by: bbingz <zzb@gxsmjx.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Neil
2026-09-14 01:44:47 -07:00
committed by GitHub
co-authored by bbingz Cursor
parent 539d4d1f32
commit 2ed89b8781
12 changed files with 115 additions and 23 deletions
@@ -64,7 +64,11 @@ function row(id: string, title: string, values: GitHubProjectFieldValue[]): GitH
}
}
function table(fields: GitHubProjectField[], rows: GitHubProjectRow[]): GitHubProjectTable {
function table(
fields: GitHubProjectField[],
rows: GitHubProjectRow[],
filter = ''
): GitHubProjectTable {
return {
project: {
id: 'PVT_1',
@@ -79,7 +83,7 @@ function table(fields: GitHubProjectField[], rows: GitHubProjectRow[]): GitHubPr
number: 2,
name: 'Roadmap',
layout: 'ROADMAP_LAYOUT',
filter: '',
filter,
fields,
groupByFields: [],
sortByFields: []
@@ -263,11 +267,22 @@ describe('ProjectRoadmap', () => {
it('reports an empty filter result instead of drawing an empty grid', () => {
render(
<ProjectRoadmap
table={table([TITLE_FIELD, START_FIELD, TARGET_FIELD], [])}
table={table([TITLE_FIELD, START_FIELD, TARGET_FIELD], [], 'status:Todo')}
fallback={<div>list</div>}
/>
)
expect(screen.getByText("No items match this view's filter.")).toBeTruthy()
expect(screen.queryByText('list')).toBeNull()
})
it('does not blame a filter an unfiltered roadmap does not have', () => {
render(
<ProjectRoadmap
table={table([TITLE_FIELD, START_FIELD, TARGET_FIELD], [])}
fallback={<div>list</div>}
/>
)
expect(screen.getByText('This view has no items yet.')).toBeTruthy()
expect(screen.queryByText("No items match this view's filter.")).toBeNull()
})
})
@@ -7,6 +7,7 @@ import { i18n, translate } from '@/i18n/i18n'
import ProjectGroupHeader from './ProjectGroupHeader'
import ProjectRoadmapBar from './ProjectRoadmapBar'
import { ProjectTitleCell } from './ProjectCellIdentity'
import { ProjectItemsEmptyState } from './ProjectViewStates'
import { formatRoadmapTick } from './roadmap-tick-format'
import { loadRoadmapZoom, saveRoadmapZoom } from './roadmap-zoom-preference'
import { groupRows, sortRows } from '../../../../shared/github/project-group-sort'
@@ -145,14 +146,7 @@ export default function ProjectRoadmap({
}
if (table.rows.length === 0) {
return (
<div className="flex min-h-[120px] items-center justify-center p-6 text-sm text-muted-foreground">
{translate(
'auto.components.github.project.ProjectViewList.4f57d2e0b1',
"No items match this view's filter."
)}
</div>
)
return <ProjectItemsEmptyState filter={view.filter} />
}
const undatedCount = table.rows.length - spans.size
@@ -5,6 +5,7 @@ import { cn } from '@/lib/utils'
import ColumnResizeHandle from './ColumnResizeHandle'
import ProjectGroupHeader from './ProjectGroupHeader'
import ProjectRow from './ProjectRow'
import { ProjectItemsEmptyState } from './ProjectViewStates'
import { groupRows, sortRows } from '../../../../shared/github/project-group-sort'
import { getAvailableColumns, loadHiddenColumns, saveHiddenColumns } from './columns'
import {
@@ -181,14 +182,7 @@ export default function ProjectViewList({
}
if (table.rows.length === 0) {
return (
<div className="flex min-h-[120px] items-center justify-center p-6 text-sm text-muted-foreground">
{translate(
'auto.components.github.project.ProjectViewList.4f57d2e0b1',
"No items match this view's filter."
)}
</div>
)
return <ProjectItemsEmptyState filter={table.selectedView.filter} />
}
// Why: the visible sort indicator reflects either the local override or the
@@ -0,0 +1,28 @@
// @vitest-environment happy-dom
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { ProjectItemsEmptyState } from './ProjectViewStates'
afterEach(cleanup)
const FILTERED_COPY = "No items match this view's filter."
const UNFILTERED_COPY = 'This view has no items yet.'
const TRANSIENCE_HINT = 'Recently added items can take a while to appear.'
describe('ProjectItemsEmptyState', () => {
it('blames the filter only when the view actually has one', () => {
render(<ProjectItemsEmptyState filter="status:Todo" />)
expect(screen.getByText(FILTERED_COPY)).toBeTruthy()
expect(screen.queryByText(UNFILTERED_COPY)).toBeNull()
})
// #12648: an unfiltered board that momentarily reads back empty must not be
// reported as a filter miss — that reads as data loss.
it.each(['', ' ', '\n\t'])('reports an unfiltered view as empty for filter %j', (filter) => {
render(<ProjectItemsEmptyState filter={filter} />)
expect(screen.getByText(UNFILTERED_COPY)).toBeTruthy()
expect(screen.getByText(TRANSIENCE_HINT)).toBeTruthy()
expect(screen.queryByText(FILTERED_COPY)).toBeNull()
})
})
@@ -225,3 +225,41 @@ export function ProjectTableSkeleton(): React.JSX.Element {
</div>
)
}
/**
* Empty result for a project view, worded from the view's own filter.
*
* Why: an unfiltered view has no filter to blame, so "no items match this
* view's filter" reads as data loss when a freshly populated board momentarily
* comes back empty (#12648). `ProjectV2.items(query:)` defaults to `""`, so
* there is no non-search request shape to fall back to — the honest remedy is
* to name the state correctly and say the emptiness may be transient.
*/
export function ProjectItemsEmptyState({ filter }: { filter: string }): React.JSX.Element {
if (filter.trim().length > 0) {
return (
<div className="flex min-h-[120px] items-center justify-center p-6 text-sm text-muted-foreground">
{translate(
'auto.components.github.project.ProjectViewList.4f57d2e0b1',
"No items match this view's filter."
)}
</div>
)
}
return (
<div className="flex min-h-[120px] flex-col items-center justify-center gap-1 p-6 text-center text-sm text-muted-foreground">
<span>
{translate(
'auto.components.github.project.ProjectViewStates.3b9c1d5e47',
'This view has no items yet.'
)}
</span>
<span className="text-xs">
{translate(
'auto.components.github.project.ProjectViewStates.7e4a2f80c6',
'Recently added items can take a while to appear.'
)}
</span>
</div>
)
}
+3 -1
View File
@@ -2584,7 +2584,9 @@
},
"ProjectViewStates": {
"ac83c45672": "Switch to a Table or Roadmap view to work with this project in Orca.",
"e4cc8b14f2": "Orca renders table and roadmap project views. This view uses a layout it cannot render yet."
"e4cc8b14f2": "Orca renders table and roadmap project views. This view uses a layout it cannot render yet.",
"3b9c1d5e47": "This view has no items yet.",
"7e4a2f80c6": "Recently added items can take a while to appear."
}
},
"GitHubMarkdownComposer": {
+4
View File
@@ -2218,6 +2218,10 @@
"7c302f8174": "Sin título"
}
}
},
"ProjectViewStates": {
"3b9c1d5e47": "Esta vista aún no tiene elementos.",
"7e4a2f80c6": "Los elementos añadidos recientemente pueden tardar un poco en aparecer."
}
},
"GitHubMarkdownComposer": {
+4
View File
@@ -2382,6 +2382,10 @@
"7c302f8174": "Sans titre"
}
}
},
"ProjectViewStates": {
"3b9c1d5e47": "Cette vue ne contient encore aucun élément.",
"7e4a2f80c6": "Les éléments ajoutés récemment peuvent mettre un moment à apparaître."
}
},
"GitHubMarkdownComposer": {
+4
View File
@@ -2218,6 +2218,10 @@
"7c302f8174": "無題"
}
}
},
"ProjectViewStates": {
"3b9c1d5e47": "このビューにはまだ項目がありません。",
"7e4a2f80c6": "最近追加した項目は、表示されるまで少し時間がかかることがあります。"
}
},
"GitHubMarkdownComposer": {
+4
View File
@@ -2223,6 +2223,10 @@
"7c302f8174": "제목 없음"
}
}
},
"ProjectViewStates": {
"3b9c1d5e47": "이 보기에는 아직 항목이 없습니다.",
"7e4a2f80c6": "최근에 추가한 항목은 표시되기까지 시간이 걸릴 수 있습니다."
}
},
"GitHubMarkdownComposer": {
+4
View File
@@ -2221,6 +2221,10 @@
"7c302f8174": "无标题"
}
}
},
"ProjectViewStates": {
"3b9c1d5e47": "此视图暂无任何项目。",
"7e4a2f80c6": "最近添加的项目可能需要一段时间才会显示。"
}
},
"GitHubMarkdownComposer": {
+4 -3
View File
@@ -81,9 +81,10 @@ export type GitHubProjectView = {
number: number
name: string
layout: GitHubProjectViewLayout
/** Normalized to '' when GitHub returns null. Why: passing null through as
* `$q` in the items query would change the query shape between filtered
* and unfiltered views; the empty string keeps the GraphQL shape stable. */
/** Normalized to '' when GitHub returns null. `ProjectV2.items(query:)` is
* declared `String = ""`, so sending '' and omitting the argument are the
* same request — there is no non-search item field to fall back to. '' is
* therefore only a UI signal: it means "this view is unfiltered". */
filter: string
fields: GitHubProjectField[]
groupByFields: GitHubProjectField[]