diff --git a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx index 6e3d6d3d1..a5802ee0d 100644 --- a/src/renderer/src/components/github-project/ProjectViewWrapper.tsx +++ b/src/renderer/src/components/github-project/ProjectViewWrapper.tsx @@ -48,6 +48,11 @@ import ProjectPicker, { type ResolvedProjectSelection } from './ProjectPicker' import ProjectViewList from './ProjectViewList' import ProjectItemSlugDialog from './ProjectItemSlugDialog' import { filterProjectTableRowsByOpenRepos } from './project-row-filtering' +import { + getNextVisibleProjectTableCache, + getVisibleProjectTable, + type CachedVisibleProjectTable +} from './project-visible-table-cache' type Props = Record @@ -308,27 +313,23 @@ export default function ProjectViewWrapper(_props: Props = {} as Props): React.J () => (table && slugIndexReady ? filterProjectTableRowsByOpenRepos(table, lookupSlug) : null), [table, slugIndexReady, lookupSlug] ) - const [lastFilteredTable, setLastFilteredTable] = useState<{ - cacheKey: string - table: GitHubProjectTable - } | null>(null) - - useEffect(() => { - if (!currentCacheKey || !table) { - setLastFilteredTable(null) - return - } - if (slugIndexReady && filteredTable) { - setLastFilteredTable({ cacheKey: currentCacheKey, table: filteredTable }) - } - }, [currentCacheKey, table, slugIndexReady, filteredTable]) - - const visibleTable = - slugIndexReady || !currentCacheKey - ? filteredTable - : lastFilteredTable?.cacheKey === currentCacheKey - ? lastFilteredTable.table - : null + const lastFilteredTableRef = useRef(null) + // Why: this cache only prevents a blank table while the repo slug index + // rebuilds; a ref preserves the previous render value without scheduling + // a second render after every filtered-table change. + lastFilteredTableRef.current = getNextVisibleProjectTableCache({ + currentCacheKey, + sourceTable: table, + slugIndexReady, + filteredTable, + previous: lastFilteredTableRef.current + }) + const visibleTable = getVisibleProjectTable({ + currentCacheKey, + slugIndexReady, + filteredTable, + cachedTable: lastFilteredTableRef.current + }) // Parent-dropped toast, once per table. useEffect(() => { diff --git a/src/renderer/src/components/github-project/project-visible-table-cache.test.ts b/src/renderer/src/components/github-project/project-visible-table-cache.test.ts new file mode 100644 index 000000000..e2bc44cca --- /dev/null +++ b/src/renderer/src/components/github-project/project-visible-table-cache.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest' +import type { GitHubProjectTable } from '../../../../shared/github-project-types' +import { + getNextVisibleProjectTableCache, + getVisibleProjectTable +} from './project-visible-table-cache' + +function table(id: string): GitHubProjectTable { + return { id } as unknown as GitHubProjectTable +} + +describe('project visible table cache', () => { + it('stores the filtered table while the slug index is ready', () => { + const sourceTable = table('source') + const filteredTable = table('filtered') + + expect( + getNextVisibleProjectTableCache({ + currentCacheKey: 'project:view', + sourceTable, + slugIndexReady: true, + filteredTable, + previous: null + }) + ).toEqual({ cacheKey: 'project:view', table: filteredTable }) + }) + + it('keeps the previous cache while the slug index is rebuilding', () => { + const previous = { cacheKey: 'project:view', table: table('previous') } + + expect( + getNextVisibleProjectTableCache({ + currentCacheKey: 'project:view', + sourceTable: table('source'), + slugIndexReady: false, + filteredTable: null, + previous + }) + ).toBe(previous) + }) + + it('drops the cache when there is no current table', () => { + const previous = { cacheKey: 'project:view', table: table('previous') } + + expect( + getNextVisibleProjectTableCache({ + currentCacheKey: null, + sourceTable: null, + slugIndexReady: false, + filteredTable: null, + previous + }) + ).toBeNull() + }) + + it('shows a matching cached table while the slug index is rebuilding', () => { + const cachedTable = { cacheKey: 'project:view', table: table('cached') } + + expect( + getVisibleProjectTable({ + currentCacheKey: 'project:view', + slugIndexReady: false, + filteredTable: null, + cachedTable + }) + ).toBe(cachedTable.table) + }) + + it('does not show stale cached data for a different cache key', () => { + const cachedTable = { cacheKey: 'other:view', table: table('cached') } + + expect( + getVisibleProjectTable({ + currentCacheKey: 'project:view', + slugIndexReady: false, + filteredTable: null, + cachedTable + }) + ).toBeNull() + }) +}) diff --git a/src/renderer/src/components/github-project/project-visible-table-cache.ts b/src/renderer/src/components/github-project/project-visible-table-cache.ts new file mode 100644 index 000000000..241101225 --- /dev/null +++ b/src/renderer/src/components/github-project/project-visible-table-cache.ts @@ -0,0 +1,34 @@ +import type { GitHubProjectTable } from '../../../../shared/github-project-types' + +export type CachedVisibleProjectTable = { + cacheKey: string + table: GitHubProjectTable +} + +export function getNextVisibleProjectTableCache(input: { + currentCacheKey: string | null + sourceTable: GitHubProjectTable | null + slugIndexReady: boolean + filteredTable: GitHubProjectTable | null + previous: CachedVisibleProjectTable | null +}): CachedVisibleProjectTable | null { + if (!input.currentCacheKey || !input.sourceTable) { + return null + } + if (input.slugIndexReady && input.filteredTable) { + return { cacheKey: input.currentCacheKey, table: input.filteredTable } + } + return input.previous +} + +export function getVisibleProjectTable(input: { + currentCacheKey: string | null + slugIndexReady: boolean + filteredTable: GitHubProjectTable | null + cachedTable: CachedVisibleProjectTable | null +}): GitHubProjectTable | null { + if (input.slugIndexReady || !input.currentCacheKey) { + return input.filteredTable + } + return input.cachedTable?.cacheKey === input.currentCacheKey ? input.cachedTable.table : null +}