commander sorting

This commit is contained in:
FTCHD
2026-07-13 03:03:16 +03:00
parent f741784ac7
commit c123727f3a
2 changed files with 116 additions and 8 deletions
+47 -5
View File
@@ -1,6 +1,6 @@
import { Button, Divider, Tooltip } from '@heroui/react' import { Button, Divider, Tooltip } from '@heroui/react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { FolderPlusIcon } from 'lucide-react' import { ChevronDownIcon, ChevronUpIcon, FolderPlusIcon } from 'lucide-react'
import { import {
forwardRef, forwardRef,
startTransition, startTransition,
@@ -352,12 +352,54 @@ const FilePanel = forwardRef<
<div className="relative flex flex-col w-full h-full overflow-hidden"> <div className="relative flex flex-col w-full h-full overflow-hidden">
<div <div
className={`sticky top-0 z-10 grid ${showPreviewColumn ? 'grid-cols-[2.5rem_1fr_6rem_9rem_11rem]' : 'grid-cols-[2.5rem_1fr_6rem_9rem_2.5rem]'} items-center py-2 bg-default-100`} className={`sticky top-0 z-10 grid ${showPreviewColumn ? 'grid-cols-[2.5rem_1fr_6rem_9rem_11rem]' : 'grid-cols-[2.5rem_1fr_6rem_9rem_2.5rem]'} items-stretch bg-default-100`}
> >
<div /> <div />
<div className="pl-2 font-semibold text-small">Name</div> {(
<div className="font-semibold text-small">Size</div> [
<div className="font-semibold text-small">Last Modified</div> { column: 'name', label: 'Name', className: 'pl-2' },
{ column: 'size', label: 'Size', className: '' },
{
column: 'modTime',
label: 'Last Modified',
className: '',
},
] as const
).map(({ column, label, className }) => {
const isSorted = nav.sortDescriptor.column === column
const nextDirection =
isSorted && nav.sortDescriptor.direction === 'ascending'
? 'descending'
: 'ascending'
const sortStatus = isSorted
? `, sorted ${nav.sortDescriptor.direction}`
: ''
return (
<div key={column}>
<button
type="button"
className={`flex items-center w-full h-full gap-1 py-2 font-semibold text-left rounded-small text-small hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary ${className}`}
aria-pressed={isSorted}
aria-label={`${label}${sortStatus}. Activate to sort ${nextDirection}.`}
onClick={() => nav.handleSort(column)}
>
<span>{label}</span>
{isSorted &&
(nav.sortDescriptor.direction === 'ascending' ? (
<ChevronUpIcon
className="size-3.5"
aria-hidden="true"
/>
) : (
<ChevronDownIcon
className="size-3.5"
aria-hidden="true"
/>
))}
</button>
</div>
)
})}
<div /> <div />
</div> </div>
+69 -3
View File
@@ -27,6 +27,11 @@ import {
serializeRemotePath, serializeRemotePath,
} from './utils' } from './utils'
const nameCollator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: 'base',
})
export default function useFileNavigation({ export default function useFileNavigation({
initialRemote, initialRemote,
initialPath, initialPath,
@@ -56,6 +61,10 @@ export default function useFileNavigation({
const [cwd, setCwd] = useState<string>(initialPath ?? '') const [cwd, setCwd] = useState<string>(initialPath ?? '')
const [pathInput, setPathInput] = useState<string>('') const [pathInput, setPathInput] = useState<string>('')
const [searchTerm, setSearchTerm] = useState<string>('') const [searchTerm, setSearchTerm] = useState<string>('')
const [sortDescriptor, setSortDescriptor] = useState<{
column: 'name' | 'size' | 'modTime'
direction: 'ascending' | 'descending'
}>({ column: 'name', direction: 'ascending' })
const [items, setItems] = useState<Entry[]>([]) const [items, setItems] = useState<Entry[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false) const [isLoading, setIsLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -88,10 +97,65 @@ export default function useFileNavigation({
const visibleItems = useMemo(() => { const visibleItems = useMemo(() => {
const base = allowFiles ? items : items.filter((it) => it.isDir) const base = allowFiles ? items : items.filter((it) => it.isDir)
if (!searchTerm) return base
const lower = searchTerm.toLowerCase() const lower = searchTerm.toLowerCase()
return base.filter((item) => item.name.toLowerCase().includes(lower)) const filtered = searchTerm
}, [allowFiles, items, searchTerm]) ? base.filter((item) => item.name.toLowerCase().includes(lower))
: base
const direction = sortDescriptor.direction === 'ascending' ? 1 : -1
return [...filtered].sort((a, b) => {
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1
const nameComparison =
nameCollator.compare(a.name, b.name) ||
a.name.localeCompare(b.name) ||
a.key.localeCompare(b.key)
if (sortDescriptor.column === 'name') return nameComparison * direction
const aValue =
sortDescriptor.column === 'size'
? typeof a.size === 'number' && a.size >= 0
? a.size
: undefined
: a.modTime
? Date.parse(a.modTime)
: undefined
const bValue =
sortDescriptor.column === 'size'
? typeof b.size === 'number' && b.size >= 0
? b.size
: undefined
: b.modTime
? Date.parse(b.modTime)
: undefined
const normalizedA =
aValue !== undefined && Number.isFinite(aValue) ? aValue : undefined
const normalizedB =
bValue !== undefined && Number.isFinite(bValue) ? bValue : undefined
if (normalizedA === undefined && normalizedB !== undefined) return 1
if (normalizedA !== undefined && normalizedB === undefined) return -1
if (
normalizedA !== undefined &&
normalizedB !== undefined &&
normalizedA !== normalizedB
) {
return (normalizedA - normalizedB) * direction
}
return nameComparison
})
}, [allowFiles, items, searchTerm, sortDescriptor])
const handleSort = useCallback((column: 'name' | 'size' | 'modTime') => {
setSortDescriptor((current) => ({
column,
direction:
current.column === column && current.direction === 'ascending'
? 'descending'
: 'ascending',
}))
}, [])
const virtualizedItems: (VirtualizedEntry | PaddingItem)[] = useMemo(() => { const virtualizedItems: (VirtualizedEntry | PaddingItem)[] = useMemo(() => {
const base: (VirtualizedEntry | PaddingItem)[] = visibleItems.map((item) => ({ const base: (VirtualizedEntry | PaddingItem)[] = visibleItems.map((item) => ({
@@ -783,6 +847,7 @@ export default function useFileNavigation({
error, error,
isUpDisabled, isUpDisabled,
searchTerm, searchTerm,
sortDescriptor,
selectedPaths, selectedPaths,
selectedCount, selectedCount,
isRemote, isRemote,
@@ -795,6 +860,7 @@ export default function useFileNavigation({
// Actions // Actions
setPathInput, setPathInput,
setSearchTerm, setSearchTerm,
handleSort,
handleNavigate, handleNavigate,
navigateUp, navigateUp,
navigateTo, navigateTo,