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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user