diff --git a/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx b/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx index 24d04bcb239..2b4c799b861 100644 --- a/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx +++ b/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx @@ -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( list} /> ) 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( + list} + /> + ) + expect(screen.getByText('This view has no items yet.')).toBeTruthy() + expect(screen.queryByText("No items match this view's filter.")).toBeNull() + }) }) diff --git a/src/renderer/src/components/github-project/ProjectRoadmap.tsx b/src/renderer/src/components/github-project/ProjectRoadmap.tsx index d06ef409dff..5e06e41d870 100644 --- a/src/renderer/src/components/github-project/ProjectRoadmap.tsx +++ b/src/renderer/src/components/github-project/ProjectRoadmap.tsx @@ -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 ( -
- {translate( - 'auto.components.github.project.ProjectViewList.4f57d2e0b1', - "No items match this view's filter." - )} -
- ) + return } const undatedCount = table.rows.length - spans.size diff --git a/src/renderer/src/components/github-project/ProjectViewList.tsx b/src/renderer/src/components/github-project/ProjectViewList.tsx index e54493418d6..cdb9e2dc487 100644 --- a/src/renderer/src/components/github-project/ProjectViewList.tsx +++ b/src/renderer/src/components/github-project/ProjectViewList.tsx @@ -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 ( -
- {translate( - 'auto.components.github.project.ProjectViewList.4f57d2e0b1', - "No items match this view's filter." - )} -
- ) + return } // Why: the visible sort indicator reflects either the local override or the diff --git a/src/renderer/src/components/github-project/ProjectViewStates.test.tsx b/src/renderer/src/components/github-project/ProjectViewStates.test.tsx new file mode 100644 index 00000000000..84033b7d40b --- /dev/null +++ b/src/renderer/src/components/github-project/ProjectViewStates.test.tsx @@ -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() + 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() + expect(screen.getByText(UNFILTERED_COPY)).toBeTruthy() + expect(screen.getByText(TRANSIENCE_HINT)).toBeTruthy() + expect(screen.queryByText(FILTERED_COPY)).toBeNull() + }) +}) diff --git a/src/renderer/src/components/github-project/ProjectViewStates.tsx b/src/renderer/src/components/github-project/ProjectViewStates.tsx index e3184878851..d2cb13bbfb4 100644 --- a/src/renderer/src/components/github-project/ProjectViewStates.tsx +++ b/src/renderer/src/components/github-project/ProjectViewStates.tsx @@ -225,3 +225,41 @@ export function ProjectTableSkeleton(): React.JSX.Element { ) } + +/** + * 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 ( +
+ {translate( + 'auto.components.github.project.ProjectViewList.4f57d2e0b1', + "No items match this view's filter." + )} +
+ ) + } + return ( +
+ + {translate( + 'auto.components.github.project.ProjectViewStates.3b9c1d5e47', + 'This view has no items yet.' + )} + + + {translate( + 'auto.components.github.project.ProjectViewStates.7e4a2f80c6', + 'Recently added items can take a while to appear.' + )} + +
+ ) +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 94ebfc81af8..81700b22ff3 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 29ddcca9bb0..61c3f812f5f 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 35bebb63736..c21e08e7a5a 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -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": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 02bfa9a9eaf..422e2b39fbc 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -2218,6 +2218,10 @@ "7c302f8174": "無題" } } + }, + "ProjectViewStates": { + "3b9c1d5e47": "このビューにはまだ項目がありません。", + "7e4a2f80c6": "最近追加した項目は、表示されるまで少し時間がかかることがあります。" } }, "GitHubMarkdownComposer": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 1b87a3d3d37..45d2d30a225 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -2223,6 +2223,10 @@ "7c302f8174": "제목 없음" } } + }, + "ProjectViewStates": { + "3b9c1d5e47": "이 보기에는 아직 항목이 없습니다.", + "7e4a2f80c6": "최근에 추가한 항목은 표시되기까지 시간이 걸릴 수 있습니다." } }, "GitHubMarkdownComposer": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index bb8ecf5ac76..f64188d7201 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2221,6 +2221,10 @@ "7c302f8174": "无标题" } } + }, + "ProjectViewStates": { + "3b9c1d5e47": "此视图暂无任何项目。", + "7e4a2f80c6": "最近添加的项目可能需要一段时间才会显示。" } }, "GitHubMarkdownComposer": { diff --git a/src/shared/github/project-types.ts b/src/shared/github/project-types.ts index 0d33dc5d976..1fcbc9e5d33 100644 --- a/src/shared/github/project-types.ts +++ b/src/shared/github/project-types.ts @@ -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[]