fix(web): truncate long project names in the automation project picker (#3274) (#3317)

Long project names in the "Existing projects" section of the
automation project picker rendered verbatim with no truncate styling,
so a single name like "A very long project name that would otherwise
wrap onto several lines" blew up the row height and made the dropdown
messy to scan. The expected behavior is a single-line label with
ellipsis, with the full name still discoverable on hover.

Add the standard truncate triad (`white-space: nowrap`,
`overflow: hidden`, `text-overflow: ellipsis`) to
`.automation-popover__label`. The parent
`.automation-popover__body` already sets `min-width: 0`, so the
ellipsis renders cleanly. Thread an optional `title` prop through
`PopoverItem` and pass each project's full name from the picker
call site, so the native hover tooltip carries the unclipped name.

Other PopoverItems with fixed in-product copy (e.g. "New project
each run") deliberately omit the title — they never exceed the row
width and the redundant tooltip would be noise.

Regression test covers the DOM contract (every project row has
`title=<full name>`, fixed rows do not); the CSS half is verified by
code review since jsdom does not apply stylesheets.
This commit is contained in:
YOMXXX 2026-05-30 12:42:21 +08:00 committed by GitHub
parent e76eb6da63
commit 9305bd1cff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 102 additions and 0 deletions

View file

@ -818,6 +818,7 @@ export function NewAutomationModal({
setPopover(null);
}}
label={p.name}
title={p.name}
/>
))}
</>
@ -1018,17 +1019,24 @@ function PopoverItem({
label,
hint,
onClick,
title,
}: {
selected?: boolean;
label: string;
hint?: string;
onClick: () => void;
// Native hover tooltip surfaced when the visible label is truncated to
// ellipsis (e.g. long project names in the picker, #3274). Optional so
// unchanged call sites with short fixed labels don't grow a noisy
// duplicate tooltip.
title?: string;
}) {
return (
<button
type="button"
className={`automation-popover__item${selected ? ' is-selected' : ''}`}
onClick={onClick}
title={title}
>
<span className="automation-popover__check">
{selected ? <Icon name="check" size={12} /> : null}

View file

@ -1444,6 +1444,20 @@
font-size: 12.5px;
font-weight: 680;
color: var(--text-strong);
/*
* Single-line truncate with ellipsis (#3274). Long project names in the
* "Existing projects" section of the automation project picker used to
* wrap across multiple lines, blowing up each row's height and making
* the dropdown hard to scan. The parent `.automation-popover__body`
* already sets `min-width: 0` so the flex child can shrink below its
* intrinsic content width these three properties complete the
* standard truncate triad. The full name is preserved via the row's
* `title` attribute (set by the picker call site) so it remains
* discoverable via the native hover tooltip.
*/
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.automation-popover__hint {

View file

@ -0,0 +1,80 @@
// @vitest-environment jsdom
//
// Regression for #3274. The automation project picker rendered long project
// names verbatim with no truncate styling, so a single long name blew up
// each row's height and made the dropdown messy to scan. The fix adds the
// single-line truncate-with-ellipsis CSS triad to `.automation-popover__label`
// and threads each project's full name through to the row's `title`
// attribute so the native hover tooltip still surfaces it. The CSS half
// is verified by code review (jsdom does not apply stylesheets); this
// test locks in the DOM contract — every existing-project row must carry
// `title=<full name>` so the tooltip exists even when the visible label
// is clipped.
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { NewAutomationModal } from '../../src/components/NewAutomationModal';
import { listPlugins } from '../../src/state/projects';
import { fetchMcpServers } from '../../src/state/mcp';
vi.mock('../../src/state/projects', () => ({
listPlugins: vi.fn().mockResolvedValue([]),
}));
vi.mock('../../src/state/mcp', () => ({
fetchMcpServers: vi.fn().mockResolvedValue({ servers: [], templates: [] }),
}));
afterEach(() => {
cleanup();
vi.resetAllMocks();
});
describe('NewAutomationModal project picker', () => {
it('exposes each project\'s full name as the row title so truncated labels still surface via tooltip (#3274)', () => {
const longName = 'A very long project name that would otherwise wrap onto several lines inside the automation picker';
render(
<NewAutomationModal
open
templates={[]}
projects={[
{ id: 'p-1', name: longName },
{ id: 'p-2', name: 'Short' },
]}
skills={[]}
connectors={[]}
onClose={() => undefined}
onSaved={() => undefined}
/>,
);
// Open the project popover. It is the only PillButton on the row that
// toggles `popover === 'project'`; the visible label is the current
// selection ("New project each run" by default) but the button still
// shows the project icon, which we use as a stable accessible cue.
const projectButton =
screen.getByRole('button', { name: /New project each run/i });
fireEvent.click(projectButton);
// Both project rows render, each with `title=<full name>` on the
// button so the native tooltip preserves the full project name even
// when the visible label is clipped by the ellipsis CSS.
const longRow = screen.getByRole('button', { name: longName });
expect(longRow.getAttribute('title')).toBe(longName);
const shortRow = screen.getByRole('button', { name: 'Short' });
expect(shortRow.getAttribute('title')).toBe('Short');
// PopoverItems with fixed in-product copy ("New project each run")
// intentionally do NOT carry a tooltip; the truncate optimisation
// is project-name-specific.
const fixedRows = screen.getAllByRole('button', {
name: /New project each run/i,
});
// The first match is the PillButton trigger we just clicked; the
// second is the PopoverItem inside the open popover.
expect(fixedRows.length).toBeGreaterThanOrEqual(2);
const popoverFixedRow = fixedRows.at(-1);
expect(popoverFixedRow?.getAttribute('title')).toBeNull();
});
});