mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(web): truncate long filenames in file list (no-preview state) (#3370)
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-staging / Deploy landing page to staging (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 2s
ci / Workspace unit tests (push) Failing after 2s
ci / Daemon workspace tests (push) Failing after 2s
ci / Web workspace tests (push) Failing after 2s
ci / Browser tests (push) Failing after 2s
ci / Build workspaces (push) Failing after 2s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped
Some checks failed
visual-baseline / Capture visual baselines (push) Waiting to run
ci / Detect CI change scopes (push) Successful in 0s
landing-page-ci / Validate landing page (push) Failing after 1s
landing-page-staging / Deploy landing page to staging (push) Has been skipped
nix-check / build (push) Failing after 2s
ci / Validate Nix flake (push) Has been skipped
ci / Preflight (push) Failing after 2s
ci / Workspace unit tests (push) Failing after 2s
ci / Daemon workspace tests (push) Failing after 2s
ci / Web workspace tests (push) Failing after 2s
ci / Browser tests (push) Failing after 2s
ci / Build workspaces (push) Failing after 2s
ci / Validate workspace (push) Failing after 0s
ci / Runtime trace (push) Has been skipped
When the design files preview pane was closed, a very long filename
would expand its `<td.df-cell-name>` and push the kind / mtime / menu
columns off-screen — the auto-layout `<table>` had no width
constraint on the cell, so the existing `text-overflow: ellipsis`
on `.df-row-name` never engaged.
The `:not(.no-preview)` overrides in `routines.css` already pinned
the row's children to `width: 100%; max-width: 100%` when a preview
pane was open, but the no-preview state — the one shown in the issue
screenshot — had no equivalent guard.
CSS:
- `.df-cell-name`: add `max-width: 0; min-width: 0` so the table
cell collapses to its column allocation in auto-layout.
- `.df-row-name-wrap`: add `max-width: 100%`.
- `.df-row-name`: add `max-width: 100%; min-width: 0` so the flex
child clamps to the wrap and the existing ellipsis engages.
JSX (DesignFilesPanel.tsx):
- Add `title={f.name}` (and the equivalent for directory rows and
live-artifact rows) on `.df-row-name`. The browser surfaces the
full filename on hover even when the visible text is truncated, so
users can read the leading characters without opening the preview
pane. `<DfPreview>` already renders the full name with
`word-break: break-word`.
Closes #3260
Validation:
- pnpm exec vitest run tests/components/DesignFilesPanel.long-name-truncate.test.tsx
→ 3/3 passed (1 was red on main: title attr was absent)
- pnpm --filter @open-design/web test → 2501/2501 passed (260 files)
- pnpm --filter @open-design/web typecheck → green
- pnpm guard → green
Note: jsdom does not measure layout, so the truncation itself can't
be asserted directly. The specs encode the structural contract the
CSS depends on (cell / wrap / name nesting + the `title` attr) so a
JSX shape change won't silently regress the fix.
This commit is contained in:
parent
af4a62b69a
commit
53fb175855
3 changed files with 144 additions and 4 deletions
|
|
@ -766,7 +766,12 @@ export function DesignFilesPanel({
|
|||
}}
|
||||
>
|
||||
<span className="df-row-name-wrap">
|
||||
<span className="df-row-name">{currentDir === '' ? f.name : f.name.slice(currentDir.length + 1)}</span>
|
||||
<span
|
||||
className="df-row-name"
|
||||
title={currentDir === '' ? f.name : f.name.slice(currentDir.length + 1)}
|
||||
>
|
||||
{currentDir === '' ? f.name : f.name.slice(currentDir.length + 1)}
|
||||
</span>
|
||||
<span className="df-row-sub">{humanBytes(f.size)}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -828,7 +833,7 @@ export function DesignFilesPanel({
|
|||
<td className="df-cell-name df-cell-openable" onClick={() => setCurrentDir(fullPath)}>
|
||||
<button type="button" className="df-row-name-btn" onClick={() => setCurrentDir(fullPath)}>
|
||||
<span className="df-row-name-wrap">
|
||||
<span className="df-row-name">{dirName}</span>
|
||||
<span className="df-row-name" title={dirName}>{dirName}</span>
|
||||
<span className="df-row-sub">{t('designFiles.folderCount', { n: count })}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -1240,7 +1245,9 @@ export function DesignFilesPanel({
|
|||
◉
|
||||
</span>
|
||||
<span className="df-row-name-wrap">
|
||||
<span className="df-row-name">{artifact.title}</span>
|
||||
<span className="df-row-name" title={artifact.title}>
|
||||
{artifact.title}
|
||||
</span>
|
||||
<span className="df-row-sub">
|
||||
<span>{t('designFiles.kindLiveArtifact')}</span>
|
||||
<LiveArtifactBadges
|
||||
|
|
|
|||
|
|
@ -384,6 +384,15 @@
|
|||
.df-cell-name {
|
||||
padding: 10px 12px 10px 0;
|
||||
vertical-align: middle;
|
||||
/* Force ellipsis truncation in auto-layout tables (#3260). Without
|
||||
`max-width: 0` and `min-width: 0`, a long `f.name` would expand
|
||||
this td and push the kind / mtime / menu cells off-screen — even
|
||||
though `.df-row-name` already has `text-overflow: ellipsis`. The
|
||||
`:not(.no-preview)` overrides in `routines.css` already pin the
|
||||
row's children to `width: 100%` when a preview pane is open; this
|
||||
baseline rule covers the no-preview state the issue reports. */
|
||||
max-width: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.df-cell-openable {
|
||||
cursor: pointer;
|
||||
|
|
@ -655,7 +664,16 @@
|
|||
.df-row-icon[data-kind="code"] { background: #fff7d8; color: #8c6700; }
|
||||
.df-row-icon[data-kind="text"] { background: var(--bg-subtle); color: var(--text-muted); }
|
||||
.df-row-icon[data-kind="sketch"] { background: var(--purple-bg); color: var(--purple); }
|
||||
.df-row-name-wrap { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.df-row-name-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
/* Constrain the wrap to the cell's content box so the ellipsis on
|
||||
`.df-row-name` engages even in the auto-layout table state
|
||||
(#3260). */
|
||||
max-width: 100%;
|
||||
}
|
||||
.df-row-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
|
@ -663,6 +681,12 @@
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* `max-width: 100%` on a flex child pairs with the wrap's
|
||||
`min-width: 0` so the long filename collapses to the cell width
|
||||
instead of pushing the kind / mtime / menu columns off-screen
|
||||
(#3260). */
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.df-rename-input {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, render } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { DesignFilesPanel } from '../../src/components/DesignFilesPanel';
|
||||
import type { ProjectFile } from '../../src/types';
|
||||
|
||||
// Regression coverage for #3260. In the no-preview state of the file
|
||||
// list, a very long filename used to expand its `<td>` and push the
|
||||
// kind / mtime / menu columns off-screen. The CSS fix locks
|
||||
// `.df-cell-name` to `max-width: 0; min-width: 0` so the auto-layout
|
||||
// table truncates the name with the existing `text-overflow: ellipsis`
|
||||
// instead of growing the cell. The JSX fix adds `title={f.name}` so the
|
||||
// browser surfaces the full filename on hover even when the visible
|
||||
// text is truncated. (`<DfPreview>` already renders the full name with
|
||||
// `word-break: break-word` for users who open the preview pane.)
|
||||
//
|
||||
// jsdom does not measure layout, so the truncation itself can't be
|
||||
// asserted directly. These specs encode the contract: the rendered DOM
|
||||
// keeps the structural classes the CSS relies on, and the `title` is
|
||||
// present on every name span so hover-tooltip is available even on the
|
||||
// very long row.
|
||||
|
||||
const lsStore = new Map<string, string>();
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => lsStore.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => { lsStore.set(key, value); },
|
||||
removeItem: (key: string) => { lsStore.delete(key); },
|
||||
clear: () => { lsStore.clear(); },
|
||||
});
|
||||
|
||||
function file(overrides: Partial<ProjectFile> & Pick<ProjectFile, 'name'>): ProjectFile {
|
||||
return {
|
||||
path: overrides.name,
|
||||
type: 'file',
|
||||
size: 1024,
|
||||
mtime: Date.now(),
|
||||
kind: 'image',
|
||||
mime: 'image/png',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderPanel(files: ProjectFile[]) {
|
||||
return render(
|
||||
<DesignFilesPanel
|
||||
projectId="test-project"
|
||||
files={files}
|
||||
liveArtifacts={[]}
|
||||
onRefreshFiles={vi.fn()}
|
||||
onOpenFile={vi.fn()}
|
||||
onOpenLiveArtifact={vi.fn()}
|
||||
onRenameFile={vi.fn()}
|
||||
onDeleteFile={vi.fn()}
|
||||
onDeleteFiles={vi.fn()}
|
||||
onUpload={vi.fn()}
|
||||
onUploadFiles={vi.fn()}
|
||||
onPaste={vi.fn()}
|
||||
onNewSketch={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
lsStore.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const LONG_NAME =
|
||||
'mpqdcf5m-A-1-year-old-boy-_standing_-with-short-black-hair_-big-eyes-with-black-pupils_-wearing-a-watermelon-shaped-helmet.jpeg';
|
||||
|
||||
describe('DesignFilesPanel long filename truncation (#3260)', () => {
|
||||
it('renders the file row for a long filename without crashing', () => {
|
||||
const { container } = renderPanel([file({ name: LONG_NAME })]);
|
||||
const row = container.querySelector(`[data-testid="design-file-row-${LONG_NAME}"]`);
|
||||
expect(row).toBeTruthy();
|
||||
});
|
||||
|
||||
it('exposes the full filename via a `title` attribute on the name span (hover tooltip)', () => {
|
||||
const { container } = renderPanel([file({ name: LONG_NAME })]);
|
||||
const nameSpan = container.querySelector('.df-row-name') as HTMLElement | null;
|
||||
expect(nameSpan).toBeTruthy();
|
||||
// The tooltip contract: hovering a truncated row reveals the full
|
||||
// filename. Without this users see "...g-helmet.jpeg" with no way
|
||||
// to read the leading characters until they open the preview pane.
|
||||
expect(nameSpan?.getAttribute('title')).toBe(LONG_NAME);
|
||||
});
|
||||
|
||||
it('keeps the truncate-friendly DOM structure (.df-cell-name > .df-row-name-btn > .df-row-name-wrap > .df-row-name)', () => {
|
||||
const { container } = renderPanel([file({ name: LONG_NAME })]);
|
||||
// The CSS fix relies on this nesting: `td.df-cell-name` constrains
|
||||
// its width, the wrap is min-width:0 / max-width:100%, and
|
||||
// `.df-row-name` carries `text-overflow: ellipsis`. If the JSX
|
||||
// shape ever changes the CSS regression risk returns silently —
|
||||
// this asserts the chain stays intact.
|
||||
const cell = container.querySelector('td.df-cell-name');
|
||||
expect(cell).toBeTruthy();
|
||||
const btn = cell!.querySelector('button.df-row-name-btn');
|
||||
expect(btn).toBeTruthy();
|
||||
const wrap = btn!.querySelector('span.df-row-name-wrap');
|
||||
expect(wrap).toBeTruthy();
|
||||
const name = wrap!.querySelector('span.df-row-name');
|
||||
expect(name).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue