fix: simplify workspace file list metadata

Show only modification time beside file names and share its space with hover actions. Remove the creation-time column and fixed minimum tree width to keep narrow workspace panels compact.

Release note: Workspace file lists now show only modification time and keep file names stable when more actions appear.
This commit is contained in:
mesalogo
2026-10-05 16:20:25 +08:00
parent 56b8b10d46
commit 4bb7573b10
5 changed files with 51 additions and 37 deletions
@@ -35,10 +35,10 @@ describe('WorkspaceFilesPanel', () => {
})
})
it('sorts siblings by real modification times, keeps directories first and omits unavailable creation times', async () => {
it('sorts siblings by modification time and shows only that time even when creation times are available', async () => {
const { container } = render(<WorkspaceFilesPanel projectId="project" changedFiles={[]} onLoadDiff={vi.fn()} onOpenFile={vi.fn()}
onListDirectory={async path => ({ path, truncated: false, entries: [
{ name: 'old', path: 'old', type: 'file', modifiedAt: '2026-01-01T00:00:00.000Z' },
{ name: 'old', path: 'old', type: 'file', modifiedAt: '2026-01-01T00:00:00.000Z', createdAt: '2025-12-01T00:00:00.000Z' },
{ name: 'new', path: 'new', type: 'file', modifiedAt: '2026-10-01T00:00:00.000Z' },
{ name: 'unknown', path: 'unknown', type: 'file' },
{ name: 'docs', path: 'docs', type: 'directory' }
@@ -50,6 +50,9 @@ describe('WorkspaceFilesPanel', () => {
expect(names()).toEqual(['docs', 'old', 'new', 'unknown'])
expect(screen.queryByRole('button', { name: '创建时间' })).not.toBeInTheDocument()
expect(container.querySelector('time[datetime="2026-01-01T00:00:00.000Z"]')).toBeInTheDocument()
expect(container.querySelectorAll('time')).toHaveLength(4)
expect(container.querySelector('time[datetime="2025-12-01T00:00:00.000Z"]')).not.toBeInTheDocument()
expect(container.querySelector('time[datetime="2026-01-01T00:00:00.000Z"]')).toHaveAttribute('title', new Date('2026-01-01T00:00:00.000Z').toLocaleString())
})
it('groups view switching and refresh, and only shows actions for the active view', async () => {
const onRefresh = vi.fn(async () => undefined)
+12 -14
View File
@@ -94,7 +94,7 @@ export function flattenFileTree(
expandedPaths: ReadonlySet<string>,
loadingPaths: ReadonlySet<string>,
errors: Readonly<Record<string, string | undefined>>,
sort: { field: 'name' | 'modifiedAt' | 'createdAt'; descending: boolean } = { field: 'name', descending: false }
sort: { field: 'name' | 'modifiedAt'; descending: boolean } = { field: 'name', descending: false }
): FileTreeRow[] {
const rows: FileTreeRow[] = []
const visit = (items: readonly WorkspaceDirectoryEntry[], depth: number): void => {
@@ -194,7 +194,7 @@ export function WorkspaceFilesPanel({
const [refreshing, setRefreshing] = useState(false)
const [importing, setImporting] = useState(false)
const importingRef = useRef(false)
const [sort, setSort] = useState<{ field: 'name' | 'modifiedAt' | 'createdAt'; descending: boolean }>({ field: 'name', descending: false })
const [sort, setSort] = useState<{ field: 'name' | 'modifiedAt'; descending: boolean }>({ field: 'name', descending: false })
const repositoryKnown = useRef<{ projectId?: string; available: boolean }>({ available: false })
useEffect(() => { if (!gitError && isRepository !== undefined) repositoryKnown.current = { projectId, available: isRepository } }, [gitError, isRepository, projectId])
const showGit = isRepository === true || (Boolean(gitError) && repositoryKnown.current.projectId === projectId && repositoryKnown.current.available)
@@ -477,7 +477,7 @@ export function WorkspaceFilesPanel({
{expanded ? <FolderOpen size={15} /> : <Folder size={15} />}
<span title={entry.path}>{entry.name}</span>
</button>
{renderTimes(entry)}
{renderModifiedTime(entry)}
{entryMenu(entry)}
</div>
)
@@ -505,20 +505,18 @@ export function WorkspaceFilesPanel({
</small>
)}
</button>
{renderTimes(entry)}
{renderModifiedTime(entry)}
{entryMenu(entry)}
</div>
)
}
const root = listings[browsedPath]
const showCreated = useMemo(() => Object.values(listings).some(listing => listing.entries.some(entry => entry.createdAt)), [listings])
const renderTimes = (entry: WorkspaceDirectoryEntry): React.JSX.Element => <>
{(['modifiedAt', ...(showCreated ? ['createdAt' as const] : [])] as const).map(field =>
<time className="workspace-files__time" key={field} dateTime={entry[field]} title={entry[field] ? new Date(entry[field]).toLocaleString() : undefined}>
{entry[field] ? new Date(entry[field]).toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '-'}
</time>)}
</>
const renderModifiedTime = (entry: WorkspaceDirectoryEntry): React.JSX.Element => (
<time className="workspace-files__time" dateTime={entry.modifiedAt} title={entry.modifiedAt ? new Date(entry.modifiedAt).toLocaleString() : undefined}>
{entry.modifiedAt ? new Date(entry.modifiedAt).toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) : '-'}
</time>
)
const treeRows = useMemo(
() => flattenFileTree(root?.entries ?? [], listings, expandedPaths, loadingPaths, errors, sort),
[errors, expandedPaths, listings, loadingPaths, root, sort]
@@ -566,7 +564,7 @@ export function WorkspaceFilesPanel({
</pre>
)
return (
<div ref={panelRef} className="workspace-files" data-created-time={showCreated}>
<div ref={panelRef} className="workspace-files">
{diff && <section className="assistant-sidebar__preview" aria-busy={!diff.value && !diff.error}>
<header>
<button ref={backRef} className="assistant-sidebar__back" type="button" onClick={() => {
@@ -667,8 +665,8 @@ export function WorkspaceFilesPanel({
<>
<div ref={treeRef} className="workspace-files__tree" onBlur={treeWindow.onBlur} onFocus={treeWindow.onFocus}>
<div className="workspace-files__columns">
{(['name', 'modifiedAt', ...(showCreated ? ['createdAt' as const] : [])] as const).map(field => <button type="button" key={field} aria-pressed={sort.field === field} onClick={() => setSort(current => ({ field, descending: current.field === field ? !current.descending : field !== 'name' }))}>
{t(field === 'name' ? 'management.name' : field === 'modifiedAt' ? 'management.modified' : 'management.created')}{sort.field === field ? (sort.descending ? ' ↓' : ' ↑') : ''}
{(['name', 'modifiedAt'] as const).map(field => <button type="button" key={field} aria-pressed={sort.field === field} onClick={() => setSort(current => ({ field, descending: current.field === field ? !current.descending : field !== 'name' }))}>
{t(field === 'name' ? 'management.name' : 'management.modified')}{sort.field === field ? (sort.descending ? ' ↓' : ' ↑') : ''}
</button>)}
</div>
{treeWindow.segments.map((segment) => {
+18 -15
View File
@@ -5563,29 +5563,23 @@ button > svg {
.workspace-files__row {
padding: var(--space-2);
padding-right: 38px;
min-width: 0;
grid-template-columns: auto auto minmax(0, 1fr) auto;
}
.workspace-files__entry {
position: relative;
display: grid;
grid-template-columns: minmax(160px, 1fr) 152px;
grid-template-columns: minmax(0, 1fr) 112px;
align-items: center;
padding-right: 32px;
}
.workspace-files__columns {
display: grid;
grid-template-columns: minmax(160px, 1fr) 152px;
padding-right: 32px;
grid-template-columns: minmax(0, 1fr) 112px;
border-bottom: 1px solid var(--border-subtle);
}
.workspace-files[data-created-time="true"] :is(.workspace-files__entry, .workspace-files__columns) {
grid-template-columns: minmax(160px, 1fr) 152px 152px;
}
.workspace-files__columns button {
min-height: 32px;
border: 0;
@@ -5596,11 +5590,21 @@ button > svg {
}
.workspace-files__time {
overflow: hidden;
padding-right: var(--space-2);
color: var(--text-muted);
font-size: var(--font-caption);
text-align: right;
text-overflow: ellipsis;
white-space: nowrap;
}
.workspace-files__entry:hover .workspace-files__time,
.workspace-files__entry:focus-within .workspace-files__time,
.workspace-files__entry:has(.workspace-files__more[aria-expanded="true"]) .workspace-files__time {
opacity: 0;
}
.workspace-files__destination {
overflow-wrap: anywhere;
color: var(--text-secondary);
@@ -5667,11 +5671,7 @@ button > svg {
}
.workspace-files__tree {
min-width: 344px;
}
.workspace-files[data-created-time="true"] .workspace-files__tree {
min-width: 496px;
min-width: 0;
}
.workspace-files__children {
@@ -19493,7 +19493,10 @@ textarea.field-control {
.workspace-git__commit { display: grid; width: 100%; padding: var(--space-2); grid-template-columns: minmax(0, 1fr); gap: var(--space-1); border-bottom: 1px solid var(--border-subtle); text-align: left; }
.workspace-git__commit strong, .workspace-git__commit small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.workspace-git__commit span, .workspace-git__commit small { color: var(--text-secondary); font-size: var(--font-caption); }
@media (hover: none), (pointer: coarse) { .workspace-files__more { opacity: 1; } }
@media (hover: none), (pointer: coarse) {
.workspace-files__more { opacity: 1; }
.workspace-files__time { padding-right: 36px; }
}
.document-result-preview {
display: flex;
flex-direction: column;