mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(web): make file output disclosure interactive
This commit is contained in:
@@ -3,9 +3,8 @@
|
||||
*
|
||||
* The first four files stay visible so artifacts are presented as results,
|
||||
* not hidden inside execution history. In larger batches only the remaining
|
||||
* files start collapsed; a changed result file also gets one direct "Open"
|
||||
* action that lifts the basename up to ProjectView so FileWorkspace focuses
|
||||
* the matching tab.
|
||||
* files start collapsed. Each visible file row owns its own Open action so the
|
||||
* header remains a pure disclosure target without a second, ambiguous button.
|
||||
*
|
||||
* The component is read-only over `events` — derivation lives in
|
||||
* `runtime/file-ops.ts` so the same logic is reachable from tests and
|
||||
@@ -64,6 +63,9 @@ export function FileOpsSummary({
|
||||
// files, only rows after the fourth start hidden; expanding reveals the
|
||||
// remainder without making the entire result set disappear by default.
|
||||
const isCollapsible = entries.length > COLLAPSE_AFTER_ENTRY_COUNT;
|
||||
const visibleEntries = isCollapsible && !expanded
|
||||
? entries.slice(0, COLLAPSE_AFTER_ENTRY_COUNT)
|
||||
: entries;
|
||||
|
||||
const counts = countFileOps(entries);
|
||||
const summaryParts: string[] = [];
|
||||
@@ -72,19 +74,6 @@ export function FileOpsSummary({
|
||||
if (counts.delete > 0) summaryParts.push(`${t('tool.delete')} ${counts.delete}`);
|
||||
if (counts.read > 0) summaryParts.push(`${t('tool.read')} ${counts.read}`);
|
||||
|
||||
// Prefer the latest changed file as the result entry point. Read-only turns
|
||||
// intentionally have no primary action: opening an arbitrary input would
|
||||
// make the execution record look like a delivery card.
|
||||
const primaryEntry = [...entries]
|
||||
.reverse()
|
||||
.find((entry) =>
|
||||
!entry.ops.includes('delete') &&
|
||||
(entry.ops.includes('write') || entry.ops.includes('edit')),
|
||||
);
|
||||
const canOpenPrimary =
|
||||
!!primaryEntry &&
|
||||
!!onRequestOpenFile &&
|
||||
(projectFileNames ? projectFileNames.has(primaryEntry.path) : true);
|
||||
const header = (
|
||||
<>
|
||||
<span className="file-ops-icon" aria-hidden>
|
||||
@@ -125,24 +114,12 @@ export function FileOpsSummary({
|
||||
{header}
|
||||
</div>
|
||||
)}
|
||||
{canOpenPrimary && primaryEntry && isCollapsible && !expanded ? (
|
||||
<button
|
||||
type="button"
|
||||
className="file-ops-primary-action"
|
||||
onClick={() => onRequestOpenFile?.(primaryEntry.path)}
|
||||
title={t('tool.openInTab', { name: primaryEntry.path })}
|
||||
data-testid={`file-ops-primary-open-${primaryEntry.path}`}
|
||||
>
|
||||
{t('assistant.openFile')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<ul className="file-ops-list" role="list">
|
||||
{entries.map((entry, index) => (
|
||||
{visibleEntries.map((entry) => (
|
||||
<FileOpRow
|
||||
key={entry.fullPath}
|
||||
entry={entry}
|
||||
hidden={isCollapsible && !expanded && index >= COLLAPSE_AFTER_ENTRY_COUNT}
|
||||
projectFileNames={projectFileNames}
|
||||
onRequestOpenFile={onRequestOpenFile}
|
||||
/>
|
||||
@@ -154,12 +131,10 @@ export function FileOpsSummary({
|
||||
|
||||
function FileOpRow({
|
||||
entry,
|
||||
hidden,
|
||||
projectFileNames,
|
||||
onRequestOpenFile,
|
||||
}: {
|
||||
entry: FileOpEntry;
|
||||
hidden?: boolean;
|
||||
projectFileNames?: Set<string> | undefined;
|
||||
onRequestOpenFile?: ((name: string) => void) | undefined;
|
||||
}) {
|
||||
@@ -172,7 +147,6 @@ function FileOpRow({
|
||||
<li
|
||||
className={`file-ops-row file-ops-row--${entry.status}`}
|
||||
data-testid={`file-ops-row-${entry.path}`}
|
||||
hidden={hidden}
|
||||
>
|
||||
<div className="file-ops-row-badges" aria-hidden>
|
||||
{entry.ops.map((op) => {
|
||||
|
||||
@@ -313,23 +313,6 @@
|
||||
.file-ops-toggle:hover { background: var(--bg-subtle); }
|
||||
.file-ops-toggle--static { cursor: default; }
|
||||
.file-ops-toggle--static:hover { background: transparent; }
|
||||
.file-ops-primary-action {
|
||||
flex: 0 0 auto;
|
||||
margin: 4px 6px 4px 0;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-panel);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: color 120ms var(--ease-out), border-color 120ms var(--ease-out), background 120ms var(--ease-out);
|
||||
}
|
||||
.file-ops-primary-action:hover {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-tint);
|
||||
color: var(--accent);
|
||||
}
|
||||
.file-ops-icon { display: inline-flex; color: var(--text-muted); }
|
||||
.file-ops-label {
|
||||
font-size: 10.5px;
|
||||
|
||||
@@ -99,13 +99,13 @@ describe('FileOpsSummary', () => {
|
||||
|
||||
const toggle = screen.getByTestId('file-ops-toggle');
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('false');
|
||||
expect(screen.getByTestId('file-ops-row-a.ts')).not.toHaveAttribute('hidden');
|
||||
expect(screen.getByTestId('file-ops-row-d.ts')).not.toHaveAttribute('hidden');
|
||||
expect(screen.getByTestId('file-ops-row-e.ts')).toHaveAttribute('hidden');
|
||||
expect(screen.getByTestId('file-ops-row-a.ts')).toBeTruthy();
|
||||
expect(screen.getByTestId('file-ops-row-d.ts')).toBeTruthy();
|
||||
expect(screen.queryByTestId('file-ops-row-e.ts')).toBeNull();
|
||||
|
||||
fireEvent.click(toggle);
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('true');
|
||||
expect(screen.getByTestId('file-ops-row-e.ts')).not.toHaveAttribute('hidden');
|
||||
expect(screen.getByTestId('file-ops-row-e.ts')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the open button only for files that are present in the project file set', () => {
|
||||
@@ -129,7 +129,7 @@ describe('FileOpsSummary', () => {
|
||||
expect(onRequestOpenFile).toHaveBeenCalledWith('a.ts');
|
||||
});
|
||||
|
||||
it('offers one direct result entry point while a large batch is collapsed', () => {
|
||||
it('keeps the header free of a redundant open action', () => {
|
||||
const onRequestOpenFile = vi.fn();
|
||||
render(
|
||||
<FileOpsSummary
|
||||
@@ -146,7 +146,8 @@ describe('FileOpsSummary', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('file-ops-primary-open-result.ts'));
|
||||
expect(screen.queryByTestId('file-ops-primary-open-result.ts')).toBeNull();
|
||||
fireEvent.click(screen.getByTestId('file-ops-row-open-result.ts'));
|
||||
expect(onRequestOpenFile).toHaveBeenCalledWith('result.ts');
|
||||
expect(screen.getByTestId('file-ops-toggle').getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user