improvement(resource): sorting and icons

This commit is contained in:
Emir Karabeg
2026-03-07 21:18:49 -08:00
parent 0b42e26f10
commit 8170488488
15 changed files with 209 additions and 22 deletions

View File

@@ -1,6 +1,5 @@
import { Fragment } from 'react'
import { Plus } from 'lucide-react'
import { Button } from '@/components/emcn'
import { Button, Plus } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
export interface BreadcrumbItem {

View File

@@ -1,6 +1,5 @@
import type { ReactNode } from 'react'
import { ArrowUpDown, ListFilter, Search } from 'lucide-react'
import { Button } from '@/components/emcn'
import { ArrowUpDown, Button, ListFilter, Search } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
interface ResourceOptionsBarProps {

View File

@@ -1,9 +1,8 @@
'use client'
import type { ReactNode } from 'react'
import { useCallback, useRef } from 'react'
import { Plus } from 'lucide-react'
import { Skeleton } from '@/components/emcn'
import { useCallback, useMemo, useRef, useState } from 'react'
import { ArrowDown, ArrowUp, Button, Plus, Skeleton } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { ResourceHeader } from './components/resource-header'
import { ResourceOptionsBar } from './components/resource-options-bar'
@@ -36,6 +35,7 @@ interface ResourceProps {
onChange: (value: string) => void
placeholder?: string
}
defaultSort: string
onSort?: () => void
onFilter?: () => void
toolbarActions?: ReactNode
@@ -59,6 +59,7 @@ export function Resource({
title,
create,
search,
defaultSort,
onSort,
onFilter,
toolbarActions,
@@ -71,6 +72,10 @@ export function Resource({
onContextMenu,
}: ResourceProps) {
const headerRef = useRef<HTMLDivElement>(null)
const [sort, setSort] = useState<{ column: string; direction: 'asc' | 'desc' }>({
column: defaultSort,
direction: 'desc',
})
const handleBodyScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
if (headerRef.current) {
@@ -78,6 +83,22 @@ export function Resource({
}
}, [])
const handleColumnSort = useCallback((columnId: string) => {
setSort((prev) => {
if (prev.column !== columnId) return { column: columnId, direction: 'asc' }
return { column: columnId, direction: prev.direction === 'asc' ? 'desc' : 'asc' }
})
}, [])
const sortedRows = useMemo(() => {
return [...rows].sort((a, b) => {
const aLabel = a.cells[sort.column]?.label ?? ''
const bLabel = b.cells[sort.column]?.label ?? ''
const cmp = aLabel.localeCompare(bLabel)
return sort.direction === 'asc' ? -cmp : cmp
})
}, [rows, sort])
return (
<div
className='flex h-full flex-1 flex-col overflow-hidden bg-white dark:bg-[var(--bg)]'
@@ -100,14 +121,23 @@ export function Resource({
<ResourceColGroup columns={columns} />
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
<tr>
{columns.map((col) => (
<th
key={col.id}
className='h-10 px-[24px] py-[10px] text-left align-middle font-base text-[var(--text-muted)]'
>
{col.header}
</th>
))}
{columns.map((col) => {
const SortIcon = sort.direction === 'asc' ? ArrowUp : ArrowDown
return (
<th key={col.id} className='h-10 px-[16px] py-[6px] text-left align-middle'>
<Button
variant='subtle'
className='px-[8px] py-[4px] font-base text-[var(--text-muted)] hover:text-[var(--text-muted)]'
onClick={() => handleColumnSort(col.id)}
>
{col.header}
{sort.column === col.id && (
<SortIcon className='ml-[4px] h-[12px] w-[12px]' />
)}
</Button>
</th>
)
})}
</tr>
</thead>
</table>
@@ -116,7 +146,7 @@ export function Resource({
<table className='w-full table-fixed text-[13px]'>
<ResourceColGroup columns={columns} />
<tbody>
{rows.map((row) => (
{sortedRows.map((row) => (
<tr
key={row.id}
data-resource-row

View File

@@ -206,6 +206,7 @@ export function Files() {
onChange: setSearchTerm,
placeholder: 'Search files...',
}}
defaultSort='created'
onSort={() => {}}
onFilter={() => {}}
columns={COLUMNS}

View File

@@ -201,6 +201,7 @@ export function Knowledge() {
onChange: setSearchQuery,
placeholder: 'Search knowledge bases...',
}}
defaultSort='created'
onSort={handleSort}
onFilter={handleFilter}
columns={COLUMNS}

View File

@@ -107,6 +107,7 @@ export function Schedules() {
onChange: setSearchQuery,
placeholder: 'Search schedules...',
}}
defaultSort='nextRun'
onSort={() => {}}
onFilter={() => {}}
columns={COLUMNS}

View File

@@ -156,6 +156,7 @@ export function Tables() {
onChange: setSearchTerm,
placeholder: 'Search tables...',
}}
defaultSort='created'
onSort={handleSort}
onFilter={handleFilter}
columns={COLUMNS}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* ArrowDown icon component
* @param props - SVG properties including className, fill, etc.
*/
export function ArrowDown(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M4 11.25L10.25 17.5L16.5 11.25' />
<path d='M10.25 3V17.5' />
</svg>
)
}

View File

@@ -0,0 +1,27 @@
import type { SVGProps } from 'react'
/**
* ArrowUpDown icon component for sort toggles
* @param props - SVG properties including className, fill, etc.
*/
export function ArrowUpDown(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M1.5 8L5.5 4L9.5 8' />
<path d='M5.5 4V16.5' />
<path d='M11 12.5L15 16.5L19 12.5' />
<path d='M15 4V16.5' />
</svg>
)
}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* ArrowUp icon component
* @param props - SVG properties including className, fill, etc.
*/
export function ArrowUp(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M4 9.25L10.25 3L16.5 9.25' />
<path d='M10.25 3V17.5' />
</svg>
)
}

View File

@@ -1,3 +1,6 @@
export { ArrowDown } from './arrow-down'
export { ArrowUp } from './arrow-up'
export { ArrowUpDown } from './arrow-up-down'
export { Blimp } from './blimp'
export { BubbleChatClose } from './bubble-chat-close'
export { BubbleChatPreview } from './bubble-chat-preview'
@@ -21,14 +24,17 @@ export { Home } from './home'
export { Key } from './key'
export { Layout } from './layout'
export { Library } from './library'
export { ListFilter } from './list-filter'
export { Loader } from './loader'
export { MoreHorizontal } from './more-horizontal'
export { NoWrap } from './no-wrap'
export { PanelLeft } from './panel-left'
export { Play, PlayOutline } from './play'
export { Plus } from './plus'
export { Redo } from './redo'
export { Rocket } from './rocket'
export { Rows3 } from './rows3'
export { Search } from './search'
export { Table } from './table'
export { TerminalWindow } from './terminal-window'
export { Trash } from './trash'

View File

@@ -0,0 +1,26 @@
import type { SVGProps } from 'react'
/**
* ListFilter icon component for filter controls
* @param props - SVG properties including className, fill, etc.
*/
export function ListFilter(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M1.5 4.5H19' />
<path d='M5 10.25H15.5' />
<path d='M8.25 16H12.25' />
</svg>
)
}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* Plus icon component
* @param props - SVG properties including className, fill, etc.
*/
export function Plus(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M10.25 3V17.5' />
<path d='M3 10.25H17.5' />
</svg>
)
}

View File

@@ -0,0 +1,25 @@
import type { SVGProps } from 'react'
/**
* Search icon component (magnifying glass)
* @param props - SVG properties including className, fill, etc.
*/
export function Search(props: SVGProps<SVGSVGElement>) {
return (
<svg
width='24'
height='24'
viewBox='-1 -2 24 24'
fill='none'
stroke='currentColor'
strokeWidth='1.75'
strokeLinecap='round'
strokeLinejoin='round'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<circle cx='8.5' cy='8.5' r='6.5' />
<path d='M13.5 13.5L18.5 18.5' />
</svg>
)
}

View File

@@ -37,12 +37,8 @@ async function listDirectory(
dirPath: string,
retryOptions?: Parameters<typeof fetchWithRetry>[2]
): Promise<string[]> {
const encodedDir = dirPath
? dirPath.split('/').map(encodeURIComponent).join('/')
: ''
const endpoint = encodedDir
? `${baseUrl}/vault/${encodedDir}/`
: `${baseUrl}/vault/`
const encodedDir = dirPath ? dirPath.split('/').map(encodeURIComponent).join('/') : ''
const endpoint = encodedDir ? `${baseUrl}/vault/${encodedDir}/` : `${baseUrl}/vault/`
const response = await fetchWithRetry(
endpoint,