mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -2218,6 +2218,10 @@
|
||||
"7c302f8174": "無題"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProjectViewStates": {
|
||||
"3b9c1d5e47": "このビューにはまだ項目がありません。",
|
||||
"7e4a2f80c6": "最近追加した項目は、表示されるまで少し時間がかかることがあります。"
|
||||
}
|
||||
},
|
||||
"GitHubMarkdownComposer": {
|
||||
|
||||
@@ -2223,6 +2223,10 @@
|
||||
"7c302f8174": "제목 없음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProjectViewStates": {
|
||||
"3b9c1d5e47": "이 보기에는 아직 항목이 없습니다.",
|
||||
"7e4a2f80c6": "최근에 추가한 항목은 표시되기까지 시간이 걸릴 수 있습니다."
|
||||
}
|
||||
},
|
||||
"GitHubMarkdownComposer": {
|
||||
|
||||
@@ -2221,6 +2221,10 @@
|
||||
"7c302f8174": "无标题"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ProjectViewStates": {
|
||||
"3b9c1d5e47": "此视图暂无任何项目。",
|
||||
"7e4a2f80c6": "最近添加的项目可能需要一段时间才会显示。"
|
||||
}
|
||||
},
|
||||
"GitHubMarkdownComposer": {
|
||||
|
||||
@@ -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[]
|
||||
|
||||
Reference in New Issue
Block a user