mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(web): show every staged design file in the Home @ picker (#3948)
* fix(web): show every staged design file in the Home @ picker The Home context picker ran staged files through .slice(0, 6) before both the Design files tab count and the rendered list, so uploading more than six design files capped the tab badge at 6 and made the 7th+ upload unreachable through the picker. Drop the cap on the file matches so the dedicated Design files tab lists every staged match (the results panel already scrolls) and its count reflects the true total. The combined All overview still previews the first six files per surface so no single section floods the picker. * fix(web): cap All-overview file count to its preview size Address review: dropping the file slice made the All tab badge count the full staged total while the All panel still previews only the first six files, relocating the count/content mismatch from the Design files tab to the All tab. Cap the All badge's file contribution to the preview size so each tab's count matches what it renders; the dedicated Design files tab keeps the true total. Extend the red spec to assert the All overview count stays aligned with its preview-sized render. --------- Co-authored-by: audit <a@b.c>
This commit is contained in:
@@ -162,6 +162,13 @@ interface Props {
|
||||
|
||||
type HomeMentionTab = 'all' | 'files' | 'plugins' | 'skills' | 'mcp' | 'connectors';
|
||||
|
||||
// In the combined "All" overview, every surface is capped to a handful of top
|
||||
// matches so no single section floods the picker. The dedicated "Design files"
|
||||
// tab is exempt: staged files are the user's own finite content, so that tab
|
||||
// lists every match (the results panel scrolls) and its count reflects the true
|
||||
// total rather than the truncated preview.
|
||||
const HOME_MENTION_ALL_TAB_PREVIEW = 6;
|
||||
|
||||
interface HomeMentionOption {
|
||||
id: string;
|
||||
icon: IconName;
|
||||
@@ -295,7 +302,6 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
|
||||
? stagedFiles
|
||||
.map((file, index) => ({ file, index }))
|
||||
.filter(({ file }) => fileMatchesQuery(file, mentionQuery))
|
||||
.slice(0, 6)
|
||||
: [],
|
||||
[mentionActive, mentionQuery, stagedFiles],
|
||||
);
|
||||
@@ -329,7 +335,11 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
|
||||
);
|
||||
const pickerOpen = mentionActive;
|
||||
const tabs: Array<{ id: HomeMentionTab; label: string; count: number }> = [
|
||||
{ id: 'all', label: t('common.all'), count: fileMatches.length + pluginMatches.length + skillMatches.length + mcpMatches.length + connectorMatches.length },
|
||||
// The All overview previews at most HOME_MENTION_ALL_TAB_PREVIEW files, so
|
||||
// its badge counts the previewed slice — not the full staged total — to keep
|
||||
// the count aligned with what that tab actually renders. The dedicated files
|
||||
// tab below lists every match and reports the true total.
|
||||
{ id: 'all', label: t('common.all'), count: Math.min(fileMatches.length, HOME_MENTION_ALL_TAB_PREVIEW) + pluginMatches.length + skillMatches.length + mcpMatches.length + connectorMatches.length },
|
||||
{ id: 'files', label: t('chat.mentionTabFiles'), count: fileMatches.length },
|
||||
{ id: 'plugins', label: t('entry.navPlugins'), count: pluginMatches.length },
|
||||
{ id: 'skills', label: t('homeHero.skills'), count: skillMatches.length },
|
||||
@@ -346,7 +356,7 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
|
||||
? {
|
||||
id: 'files',
|
||||
label: t('chat.mentionSectionFiles'),
|
||||
options: fileMatches.map(({ file, index }) => ({
|
||||
options: (mentionTab === 'files' ? fileMatches : fileMatches.slice(0, HOME_MENTION_ALL_TAB_PREVIEW)).map(({ file, index }) => ({
|
||||
id: `file-${index}-${file.name}`,
|
||||
icon: isImageFile(file) ? 'image' : 'file',
|
||||
title: file.name,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
// Red spec for the Home "@" picker capping staged design files at 6.
|
||||
//
|
||||
// Repro: a user stages more than six design files, then opens the context
|
||||
// picker. On `origin/main` the "Design files" surface ran the staged files
|
||||
// through `.slice(0, 6)` BEFORE both the tab count and the rendered list, so
|
||||
// the tab badge read "6" and only six files were ever pickable — the 7th+
|
||||
// upload was unreachable through the picker. The dedicated Design files tab
|
||||
// must instead list every staged match and report the true total.
|
||||
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { HomeView } from '../../src/components/HomeView';
|
||||
import { setHomeHeroPrompt } from '../helpers/home-hero-lexical';
|
||||
|
||||
async function settle() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function stubContextFetch() {
|
||||
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
||||
if (typeof url === 'string' && url === '/api/plugins') {
|
||||
return new Response(JSON.stringify({ plugins: [] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
||||
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('HomeView design-files mention picker', () => {
|
||||
it('lists every staged design file and counts the true total when more than six are uploaded', async () => {
|
||||
stubContextFetch();
|
||||
|
||||
const files = Array.from(
|
||||
{ length: 7 },
|
||||
(_, i) => new File(['x'], `design-${i + 1}.png`, { type: 'image/png' }),
|
||||
);
|
||||
|
||||
render(
|
||||
<HomeView
|
||||
projects={[]}
|
||||
onSubmit={() => undefined}
|
||||
onOpenProject={() => undefined}
|
||||
onViewAllProjects={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = await screen.findByTestId('home-hero-input');
|
||||
// Stage all seven uploads in one paste (Lexical's PastePlugin reads
|
||||
// `clipboardData.files`).
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
files,
|
||||
items: files.map((file) => ({ kind: 'file', getAsFile: () => file })),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('design-1.png')).toBeTruthy());
|
||||
|
||||
// Open the context picker with a query that matches every staged file.
|
||||
setHomeHeroPrompt('@design');
|
||||
await settle();
|
||||
|
||||
// The "Design files" tab badge must report the true number staged (7),
|
||||
// not the truncated preview (6).
|
||||
const filesTab = await screen.findByRole('tab', { name: /design files/i });
|
||||
expect(filesTab.textContent).toContain('7');
|
||||
|
||||
// Switching to the dedicated Design files tab must surface every match,
|
||||
// including the 7th upload that the old `.slice(0, 6)` dropped.
|
||||
fireEvent.click(filesTab);
|
||||
await settle();
|
||||
|
||||
const picker = screen.getByTestId('home-hero-plugin-picker');
|
||||
for (let i = 1; i <= 7; i += 1) {
|
||||
expect(within(picker).getByText(`design-${i}.png`)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the All overview count aligned with its preview-sized render', async () => {
|
||||
stubContextFetch();
|
||||
|
||||
const files = Array.from(
|
||||
{ length: 7 },
|
||||
(_, i) => new File(['x'], `design-${i + 1}.png`, { type: 'image/png' }),
|
||||
);
|
||||
|
||||
render(
|
||||
<HomeView
|
||||
projects={[]}
|
||||
onSubmit={() => undefined}
|
||||
onOpenProject={() => undefined}
|
||||
onViewAllProjects={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = await screen.findByTestId('home-hero-input');
|
||||
fireEvent.paste(input, {
|
||||
clipboardData: {
|
||||
files,
|
||||
items: files.map((file) => ({ kind: 'file', getAsFile: () => file })),
|
||||
},
|
||||
});
|
||||
await waitFor(() => expect(screen.getByText('design-1.png')).toBeTruthy());
|
||||
|
||||
setHomeHeroPrompt('@design');
|
||||
await settle();
|
||||
|
||||
// The default All overview previews only the first six files, so its badge
|
||||
// must read 6 — not the full 7 — and exactly six file options render. This
|
||||
// prevents the count/content mismatch from relocating to the All tab.
|
||||
const allTab = await screen.findByRole('tab', { name: /^all/i });
|
||||
expect(allTab.textContent).toContain('6');
|
||||
expect(allTab.textContent).not.toContain('7');
|
||||
|
||||
const picker = screen.getByTestId('home-hero-plugin-picker');
|
||||
const shown = Array.from({ length: 7 }, (_, i) => `design-${i + 1}.png`).filter(
|
||||
(name) => within(picker).queryByText(name) !== null,
|
||||
);
|
||||
expect(shown).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user