mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-22 05:18:08 -05:00
updates
This commit is contained in:
@@ -3,13 +3,13 @@
|
||||
import { Trash2, X } from 'lucide-react'
|
||||
import { Button } from '@/components/emcn'
|
||||
|
||||
interface TableActionBarProps {
|
||||
interface ActionBarProps {
|
||||
selectedCount: number
|
||||
onDelete: () => void
|
||||
onClearSelection: () => void
|
||||
}
|
||||
|
||||
export function TableActionBar({ selectedCount, onDelete, onClearSelection }: TableActionBarProps) {
|
||||
export function ActionBar({ selectedCount, onDelete, onClearSelection }: ActionBarProps) {
|
||||
return (
|
||||
<div className='flex h-[36px] shrink-0 items-center justify-between border-[var(--border)] border-b bg-[var(--surface-4)] px-[16px]'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
@@ -3,11 +3,11 @@ import { Button, TableCell, TableRow } from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
|
||||
interface TableLoadingRowsProps {
|
||||
interface LoadingRowsProps {
|
||||
columns: ColumnDefinition[]
|
||||
}
|
||||
|
||||
export function TableLoadingRows({ columns }: TableLoadingRowsProps) {
|
||||
export function LoadingRows({ columns }: LoadingRowsProps) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: 25 }).map((_, rowIndex) => (
|
||||
@@ -43,13 +43,13 @@ export function TableLoadingRows({ columns }: TableLoadingRowsProps) {
|
||||
)
|
||||
}
|
||||
|
||||
interface TableEmptyRowsProps {
|
||||
interface EmptyRowsProps {
|
||||
columnCount: number
|
||||
hasFilter: boolean
|
||||
onAddRow: () => void
|
||||
}
|
||||
|
||||
export function TableEmptyRows({ columnCount, hasFilter, onAddRow }: TableEmptyRowsProps) {
|
||||
export function EmptyRows({ columnCount, hasFilter, onAddRow }: EmptyRowsProps) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columnCount + 1} className='h-[160px]'>
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
import { STRING_TRUNCATE_LENGTH } from '../constants'
|
||||
import type { CellViewerData } from '../types'
|
||||
import { STRING_TRUNCATE_LENGTH } from '../lib/constants'
|
||||
import type { CellViewerData } from '../lib/types'
|
||||
|
||||
interface TableCellRendererProps {
|
||||
interface CellRendererProps {
|
||||
value: unknown
|
||||
column: ColumnDefinition
|
||||
onCellClick: (columnName: string, value: unknown, type: CellViewerData['type']) => void
|
||||
}
|
||||
|
||||
export function TableCellRenderer({ value, column, onCellClick }: TableCellRendererProps) {
|
||||
export function CellRenderer({ value, column, onCellClick }: CellRendererProps) {
|
||||
const isNull = value === null || value === undefined
|
||||
|
||||
if (isNull) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Copy, X } from 'lucide-react'
|
||||
import { Badge, Button, Modal, ModalBody, ModalContent } from '@/components/emcn'
|
||||
import type { CellViewerData } from '../types'
|
||||
import type { CellViewerData } from '../lib/types'
|
||||
|
||||
interface CellViewerModalProps {
|
||||
cellViewer: CellViewerData | null
|
||||
@@ -6,16 +6,16 @@ import {
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
import type { ContextMenuState } from '../types'
|
||||
import type { ContextMenuState } from '../lib/types'
|
||||
|
||||
interface RowContextMenuProps {
|
||||
interface ContextMenuProps {
|
||||
contextMenu: ContextMenuState
|
||||
onClose: () => void
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
export function RowContextMenu({ contextMenu, onClose, onEdit, onDelete }: RowContextMenuProps) {
|
||||
export function ContextMenu({ contextMenu, onClose, onEdit, onDelete }: ContextMenuProps) {
|
||||
return (
|
||||
<Popover
|
||||
open={contextMenu.isOpen}
|
||||
@@ -2,7 +2,7 @@ import { Info, RefreshCw } from 'lucide-react'
|
||||
import { Badge, Button, Tooltip } from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
||||
interface TableHeaderBarProps {
|
||||
interface HeaderBarProps {
|
||||
tableName: string
|
||||
totalCount: number
|
||||
isLoading: boolean
|
||||
@@ -11,14 +11,14 @@ interface TableHeaderBarProps {
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function TableHeaderBar({
|
||||
export function HeaderBar({
|
||||
tableName,
|
||||
totalCount,
|
||||
isLoading,
|
||||
onNavigateBack,
|
||||
onShowSchema,
|
||||
onRefresh,
|
||||
}: TableHeaderBarProps) {
|
||||
}: HeaderBarProps) {
|
||||
return (
|
||||
<div className='flex h-[48px] shrink-0 items-center justify-between border-[var(--border)] border-b px-[16px]'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
@@ -1,3 +1,11 @@
|
||||
export * from './table-action-bar'
|
||||
export * from './table-query-builder'
|
||||
export * from './table-row-modal'
|
||||
export * from './action-bar'
|
||||
export * from './body-states'
|
||||
export * from './cell-renderer'
|
||||
export * from './cell-viewer-modal'
|
||||
export * from './context-menu'
|
||||
export * from './header-bar'
|
||||
export * from './pagination'
|
||||
export * from './query-builder'
|
||||
export * from './row-modal'
|
||||
export * from './schema-modal'
|
||||
export * from './table-viewer'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@/components/emcn'
|
||||
|
||||
interface TablePaginationProps {
|
||||
interface PaginationProps {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
totalCount: number
|
||||
@@ -8,13 +8,13 @@ interface TablePaginationProps {
|
||||
onNextPage: () => void
|
||||
}
|
||||
|
||||
export function TablePagination({
|
||||
export function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalCount,
|
||||
onPreviousPage,
|
||||
onNextPage,
|
||||
}: TablePaginationProps) {
|
||||
}: PaginationProps) {
|
||||
if (totalPages <= 1) return null
|
||||
|
||||
return (
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { X } from 'lucide-react'
|
||||
import { Button, Combobox, Input } from '@/components/emcn'
|
||||
import type { FilterRule } from '@/lib/table/filters/constants'
|
||||
|
||||
interface FilterRowProps {
|
||||
rule: FilterRule
|
||||
index: number
|
||||
columnOptions: Array<{ value: string; label: string }>
|
||||
comparisonOptions: Array<{ value: string; label: string }>
|
||||
logicalOptions: Array<{ value: string; label: string }>
|
||||
onUpdate: (id: string, field: keyof FilterRule, value: string) => void
|
||||
onRemove: (id: string) => void
|
||||
onApply: () => void
|
||||
}
|
||||
|
||||
export function FilterRow({
|
||||
rule,
|
||||
index,
|
||||
columnOptions,
|
||||
comparisonOptions,
|
||||
logicalOptions,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
onApply,
|
||||
}: FilterRowProps) {
|
||||
return (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => onRemove(rule.id)}
|
||||
className='h-[28px] w-[28px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
|
||||
<div className='w-[80px] shrink-0'>
|
||||
{index === 0 ? (
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={[{ value: 'where', label: 'where' }]}
|
||||
value='where'
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={logicalOptions}
|
||||
value={rule.logicalOperator}
|
||||
onChange={(value) => onUpdate(rule.id, 'logicalOperator', value as 'and' | 'or')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='w-[140px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={columnOptions}
|
||||
value={rule.column}
|
||||
onChange={(value) => onUpdate(rule.id, 'column', value)}
|
||||
placeholder='Column'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='w-[130px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={comparisonOptions}
|
||||
value={rule.operator}
|
||||
onChange={(value) => onUpdate(rule.id, 'operator', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
className='h-[28px] min-w-[200px] flex-1 text-[12px]'
|
||||
value={rule.value}
|
||||
onChange={(e) => onUpdate(rule.id, 'value', e.target.value)}
|
||||
placeholder='Value'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onApply()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowUpAZ, Loader2, Plus } from 'lucide-react'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { Button } from '@/components/emcn'
|
||||
import type { FilterRule, SortRule } from '@/lib/table/filters/constants'
|
||||
import { useFilterBuilder } from '@/lib/table/filters/use-builder'
|
||||
import { filterRulesToFilter, sortRuleToSort } from '@/lib/table/filters/utils'
|
||||
import type { ColumnDefinition } from '@/lib/table/types'
|
||||
import type { QueryOptions } from '../../lib/types'
|
||||
import { FilterRow } from './filter-row'
|
||||
import { SortRow } from './sort-row'
|
||||
|
||||
type Column = Pick<ColumnDefinition, 'name' | 'type'>
|
||||
|
||||
interface QueryBuilderProps {
|
||||
columns: Column[]
|
||||
onApply: (options: QueryOptions) => void
|
||||
onAddRow: () => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function QueryBuilder({ columns, onApply, onAddRow, isLoading = false }: QueryBuilderProps) {
|
||||
const [rules, setRules] = useState<FilterRule[]>([])
|
||||
const [sortRule, setSortRule] = useState<SortRule | null>(null)
|
||||
|
||||
const columnOptions = useMemo(
|
||||
() => columns.map((col) => ({ value: col.name, label: col.name })),
|
||||
[columns]
|
||||
)
|
||||
|
||||
const {
|
||||
comparisonOptions,
|
||||
logicalOptions,
|
||||
sortDirectionOptions,
|
||||
addRule: handleAddRule,
|
||||
removeRule: handleRemoveRule,
|
||||
updateRule: handleUpdateRule,
|
||||
} = useFilterBuilder({
|
||||
columns: columnOptions,
|
||||
rules,
|
||||
setRules,
|
||||
})
|
||||
|
||||
const handleAddSort = useCallback(() => {
|
||||
setSortRule({
|
||||
id: nanoid(),
|
||||
column: columns[0]?.name || '',
|
||||
direction: 'asc',
|
||||
})
|
||||
}, [columns])
|
||||
|
||||
const handleRemoveSort = useCallback(() => {
|
||||
setSortRule(null)
|
||||
}, [])
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
const filter = filterRulesToFilter(rules)
|
||||
const sort = sortRuleToSort(sortRule)
|
||||
onApply({ filter, sort })
|
||||
}, [rules, sortRule, onApply])
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setRules([])
|
||||
setSortRule(null)
|
||||
onApply({
|
||||
filter: null,
|
||||
sort: null,
|
||||
})
|
||||
}, [onApply])
|
||||
|
||||
const hasChanges = rules.length > 0 || sortRule !== null
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{rules.map((rule, index) => (
|
||||
<FilterRow
|
||||
key={rule.id}
|
||||
rule={rule}
|
||||
index={index}
|
||||
columnOptions={columnOptions}
|
||||
comparisonOptions={comparisonOptions}
|
||||
logicalOptions={logicalOptions}
|
||||
onUpdate={handleUpdateRule}
|
||||
onRemove={handleRemoveRule}
|
||||
onApply={handleApply}
|
||||
/>
|
||||
))}
|
||||
|
||||
{sortRule && (
|
||||
<SortRow
|
||||
sortRule={sortRule}
|
||||
columnOptions={columnOptions}
|
||||
sortDirectionOptions={sortDirectionOptions}
|
||||
onChange={setSortRule}
|
||||
onRemove={handleRemoveSort}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Button variant='default' size='sm' onClick={onAddRow}>
|
||||
<Plus className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add row
|
||||
</Button>
|
||||
|
||||
<Button variant='default' size='sm' onClick={handleAddRule}>
|
||||
<Plus className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add filter
|
||||
</Button>
|
||||
|
||||
{!sortRule && (
|
||||
<Button variant='default' size='sm' onClick={handleAddSort}>
|
||||
<ArrowUpAZ className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add sort
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasChanges && (
|
||||
<>
|
||||
<Button variant='default' size='sm' onClick={handleApply} disabled={isLoading}>
|
||||
{isLoading && <Loader2 className='mr-[4px] h-[12px] w-[12px] animate-spin' />}
|
||||
{isLoading ? 'Applying...' : 'Apply'}
|
||||
</Button>
|
||||
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className='text-[12px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import { ArrowDownAZ, ArrowUpAZ, X } from 'lucide-react'
|
||||
import { Button, Combobox } from '@/components/emcn'
|
||||
import type { SortRule } from '@/lib/table/filters/constants'
|
||||
|
||||
interface SortRowProps {
|
||||
sortRule: SortRule
|
||||
columnOptions: Array<{ value: string; label: string }>
|
||||
sortDirectionOptions: Array<{ value: string; label: string }>
|
||||
onChange: (rule: SortRule | null) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
export function SortRow({
|
||||
sortRule,
|
||||
columnOptions,
|
||||
sortDirectionOptions,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: SortRowProps) {
|
||||
return (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={onRemove}
|
||||
className='h-[28px] w-[28px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
|
||||
<div className='w-[80px] shrink-0'>
|
||||
<Combobox size='sm' options={[{ value: 'order', label: 'order' }]} value='order' disabled />
|
||||
</div>
|
||||
|
||||
<div className='w-[140px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={columnOptions}
|
||||
value={sortRule.column}
|
||||
onChange={(value) => onChange({ ...sortRule, column: value })}
|
||||
placeholder='Column'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='w-[130px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={sortDirectionOptions}
|
||||
value={sortRule.direction}
|
||||
onChange={(value) => onChange({ ...sortRule, direction: value as 'asc' | 'desc' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center text-[12px] text-[var(--text-tertiary)]'>
|
||||
{sortRule.direction === 'asc' ? (
|
||||
<ArrowUpAZ className='h-[14px] w-[14px]' />
|
||||
) : (
|
||||
<ArrowDownAZ className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
import { Input } from '@/components/ui/input'
|
||||
import type { ColumnDefinition, TableInfo, TableRow } from '@/lib/table'
|
||||
|
||||
const logger = createLogger('TableRowModal')
|
||||
const logger = createLogger('RowModal')
|
||||
|
||||
export interface TableRowModalProps {
|
||||
export interface RowModalProps {
|
||||
mode: 'add' | 'edit' | 'delete'
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
@@ -92,15 +92,7 @@ function formatValueForInput(value: unknown, type: string): string {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export function TableRowModal({
|
||||
mode,
|
||||
isOpen,
|
||||
onClose,
|
||||
table,
|
||||
row,
|
||||
rowIds,
|
||||
onSuccess,
|
||||
}: TableRowModalProps) {
|
||||
export function RowModal({ mode, isOpen, onClose, table, row, rowIds, onSuccess }: RowModalProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
@@ -13,15 +13,15 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/emcn'
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
import { getTypeBadgeVariant } from '../utils'
|
||||
import { getTypeBadgeVariant } from '../lib/utils'
|
||||
|
||||
interface SchemaViewerModalProps {
|
||||
interface SchemaModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
columns: ColumnDefinition[]
|
||||
}
|
||||
|
||||
export function SchemaViewerModal({ isOpen, onClose, columns }: SchemaViewerModalProps) {
|
||||
export function SchemaModal({ isOpen, onClose, columns }: SchemaModalProps) {
|
||||
return (
|
||||
<Modal open={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent className='w-[500px] duration-100'>
|
||||
@@ -1,288 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowDownAZ, ArrowUpAZ, Loader2, Plus, X } from 'lucide-react'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { Button, Combobox, Input } from '@/components/emcn'
|
||||
import type { FilterRule, SortRule } from '@/lib/table/filters/constants'
|
||||
import { useFilterBuilder } from '@/lib/table/filters/use-builder'
|
||||
import { filterRulesToFilter, sortRuleToSort } from '@/lib/table/filters/utils'
|
||||
import type { ColumnDefinition, Filter, Sort } from '@/lib/table/types'
|
||||
|
||||
export interface BuilderQueryResult {
|
||||
filter: Filter | null
|
||||
sort: Sort | null
|
||||
}
|
||||
|
||||
type Column = Pick<ColumnDefinition, 'name' | 'type'>
|
||||
|
||||
interface TableQueryBuilderProps {
|
||||
columns: Column[]
|
||||
onApply: (options: BuilderQueryResult) => void
|
||||
onAddRow: () => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export function TableQueryBuilder({
|
||||
columns,
|
||||
onApply,
|
||||
onAddRow,
|
||||
isLoading = false,
|
||||
}: TableQueryBuilderProps) {
|
||||
const [rules, setRules] = useState<FilterRule[]>([])
|
||||
const [sortRule, setSortRule] = useState<SortRule | null>(null)
|
||||
|
||||
const columnOptions = useMemo(
|
||||
() => columns.map((col) => ({ value: col.name, label: col.name })),
|
||||
[columns]
|
||||
)
|
||||
|
||||
const {
|
||||
comparisonOptions,
|
||||
logicalOptions,
|
||||
sortDirectionOptions,
|
||||
addRule: handleAddRule,
|
||||
removeRule: handleRemoveRule,
|
||||
updateRule: handleUpdateRule,
|
||||
} = useFilterBuilder({
|
||||
columns: columnOptions,
|
||||
rules,
|
||||
setRules,
|
||||
})
|
||||
|
||||
const handleAddSort = useCallback(() => {
|
||||
setSortRule({
|
||||
id: nanoid(),
|
||||
column: columns[0]?.name || '',
|
||||
direction: 'asc',
|
||||
})
|
||||
}, [columns])
|
||||
|
||||
const handleRemoveSort = useCallback(() => {
|
||||
setSortRule(null)
|
||||
}, [])
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
const filter = filterRulesToFilter(rules)
|
||||
const sort = sortRuleToSort(sortRule)
|
||||
onApply({ filter, sort })
|
||||
}, [rules, sortRule, onApply])
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setRules([])
|
||||
setSortRule(null)
|
||||
onApply({
|
||||
filter: null,
|
||||
sort: null,
|
||||
})
|
||||
}, [onApply])
|
||||
|
||||
const hasChanges = rules.length > 0 || sortRule !== null
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{rules.map((rule, index) => (
|
||||
<FilterRuleRow
|
||||
key={rule.id}
|
||||
rule={rule}
|
||||
index={index}
|
||||
columnOptions={columnOptions}
|
||||
comparisonOptions={comparisonOptions}
|
||||
logicalOptions={logicalOptions}
|
||||
onUpdate={handleUpdateRule}
|
||||
onRemove={handleRemoveRule}
|
||||
onApply={handleApply}
|
||||
/>
|
||||
))}
|
||||
|
||||
{sortRule && (
|
||||
<SortRuleRow
|
||||
sortRule={sortRule}
|
||||
columnOptions={columnOptions}
|
||||
sortDirectionOptions={sortDirectionOptions}
|
||||
onChange={setSortRule}
|
||||
onRemove={handleRemoveSort}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Button variant='default' size='sm' onClick={onAddRow}>
|
||||
<Plus className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add row
|
||||
</Button>
|
||||
|
||||
<Button variant='default' size='sm' onClick={handleAddRule}>
|
||||
<Plus className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add filter
|
||||
</Button>
|
||||
|
||||
{!sortRule && (
|
||||
<Button variant='default' size='sm' onClick={handleAddSort}>
|
||||
<ArrowUpAZ className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add sort
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasChanges && (
|
||||
<>
|
||||
<Button variant='default' size='sm' onClick={handleApply} disabled={isLoading}>
|
||||
{isLoading && <Loader2 className='mr-[4px] h-[12px] w-[12px] animate-spin' />}
|
||||
{isLoading ? 'Applying...' : 'Apply'}
|
||||
</Button>
|
||||
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className='text-[12px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterRuleRowProps {
|
||||
rule: FilterRule
|
||||
index: number
|
||||
columnOptions: Array<{ value: string; label: string }>
|
||||
comparisonOptions: Array<{ value: string; label: string }>
|
||||
logicalOptions: Array<{ value: string; label: string }>
|
||||
onUpdate: (id: string, field: keyof FilterRule, value: string) => void
|
||||
onRemove: (id: string) => void
|
||||
onApply: () => void
|
||||
}
|
||||
|
||||
function FilterRuleRow({
|
||||
rule,
|
||||
index,
|
||||
columnOptions,
|
||||
comparisonOptions,
|
||||
logicalOptions,
|
||||
onUpdate,
|
||||
onRemove,
|
||||
onApply,
|
||||
}: FilterRuleRowProps) {
|
||||
return (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => onRemove(rule.id)}
|
||||
className='h-[28px] w-[28px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
|
||||
<div className='w-[80px] shrink-0'>
|
||||
{index === 0 ? (
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={[{ value: 'where', label: 'where' }]}
|
||||
value='where'
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={logicalOptions}
|
||||
value={rule.logicalOperator}
|
||||
onChange={(value) => onUpdate(rule.id, 'logicalOperator', value as 'and' | 'or')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='w-[140px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={columnOptions}
|
||||
value={rule.column}
|
||||
onChange={(value) => onUpdate(rule.id, 'column', value)}
|
||||
placeholder='Column'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='w-[130px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={comparisonOptions}
|
||||
value={rule.operator}
|
||||
onChange={(value) => onUpdate(rule.id, 'operator', value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
className='h-[28px] min-w-[200px] flex-1 text-[12px]'
|
||||
value={rule.value}
|
||||
onChange={(e) => onUpdate(rule.id, 'value', e.target.value)}
|
||||
placeholder='Value'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onApply()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SortRuleRowProps {
|
||||
sortRule: SortRule
|
||||
columnOptions: Array<{ value: string; label: string }>
|
||||
sortDirectionOptions: Array<{ value: string; label: string }>
|
||||
onChange: (rule: SortRule | null) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
function SortRuleRow({
|
||||
sortRule,
|
||||
columnOptions,
|
||||
sortDirectionOptions,
|
||||
onChange,
|
||||
onRemove,
|
||||
}: SortRuleRowProps) {
|
||||
return (
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={onRemove}
|
||||
className='h-[28px] w-[28px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
|
||||
<div className='w-[80px] shrink-0'>
|
||||
<Combobox size='sm' options={[{ value: 'order', label: 'order' }]} value='order' disabled />
|
||||
</div>
|
||||
|
||||
<div className='w-[140px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={columnOptions}
|
||||
value={sortRule.column}
|
||||
onChange={(value) => onChange({ ...sortRule, column: value })}
|
||||
placeholder='Column'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='w-[130px] shrink-0'>
|
||||
<Combobox
|
||||
size='sm'
|
||||
options={sortDirectionOptions}
|
||||
value={sortRule.direction}
|
||||
onChange={(value) => onChange({ ...sortRule, direction: value as 'asc' | 'desc' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center text-[12px] text-[var(--text-tertiary)]'>
|
||||
{sortRule.direction === 'asc' ? (
|
||||
<ArrowUpAZ className='h-[14px] w-[14px]' />
|
||||
) : (
|
||||
<ArrowDownAZ className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,21 +14,20 @@ import {
|
||||
} from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { TableRow as TableRowType } from '@/lib/table'
|
||||
import { type QueryOptions, TableActionBar, TableQueryBuilder, TableRowModal } from '../components'
|
||||
import {
|
||||
CellViewerModal,
|
||||
RowContextMenu,
|
||||
SchemaViewerModal,
|
||||
TableCellRenderer,
|
||||
TableEmptyRows,
|
||||
TableHeaderBar,
|
||||
TableLoadingRows,
|
||||
TablePagination,
|
||||
} from './components'
|
||||
import { useContextMenu, useRowSelection, useTableData } from './hooks'
|
||||
import type { CellViewerData } from './types'
|
||||
import { useContextMenu, useRowSelection, useTableData } from '../hooks'
|
||||
import type { CellViewerData, QueryOptions } from '../lib/types'
|
||||
import { ActionBar } from './action-bar'
|
||||
import { EmptyRows, LoadingRows } from './body-states'
|
||||
import { CellRenderer } from './cell-renderer'
|
||||
import { CellViewerModal } from './cell-viewer-modal'
|
||||
import { ContextMenu } from './context-menu'
|
||||
import { HeaderBar } from './header-bar'
|
||||
import { Pagination } from './pagination'
|
||||
import { QueryBuilder } from './query-builder'
|
||||
import { RowModal } from './row-modal'
|
||||
import { SchemaModal } from './schema-modal'
|
||||
|
||||
export function TableDataViewer() {
|
||||
export function TableViewer() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -146,7 +145,7 @@ export function TableDataViewer() {
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col'>
|
||||
<TableHeaderBar
|
||||
<HeaderBar
|
||||
tableName={tableData.name}
|
||||
totalCount={totalCount}
|
||||
isLoading={isLoadingRows}
|
||||
@@ -156,7 +155,7 @@ export function TableDataViewer() {
|
||||
/>
|
||||
|
||||
<div className='flex shrink-0 flex-col gap-[8px] border-[var(--border)] border-b px-[16px] py-[10px]'>
|
||||
<TableQueryBuilder
|
||||
<QueryBuilder
|
||||
columns={columns}
|
||||
onApply={handleApplyQueryOptions}
|
||||
onAddRow={handleAddRow}
|
||||
@@ -168,7 +167,7 @@ export function TableDataViewer() {
|
||||
</div>
|
||||
|
||||
{hasSelection && (
|
||||
<TableActionBar
|
||||
<ActionBar
|
||||
selectedCount={selectedCount}
|
||||
onDelete={handleDeleteSelected}
|
||||
onClearSelection={clearSelection}
|
||||
@@ -199,9 +198,9 @@ export function TableDataViewer() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoadingRows ? (
|
||||
<TableLoadingRows columns={columns} />
|
||||
<LoadingRows columns={columns} />
|
||||
) : rows.length === 0 ? (
|
||||
<TableEmptyRows
|
||||
<EmptyRows
|
||||
columnCount={columns.length}
|
||||
hasFilter={!!queryOptions.filter}
|
||||
onAddRow={handleAddRow}
|
||||
@@ -226,7 +225,7 @@ export function TableDataViewer() {
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.name}>
|
||||
<div className='max-w-[300px] truncate text-[13px]'>
|
||||
<TableCellRenderer
|
||||
<CellRenderer
|
||||
value={row.data[column.name]}
|
||||
column={column}
|
||||
onCellClick={handleCellClick}
|
||||
@@ -241,7 +240,7 @@ export function TableDataViewer() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<TablePagination
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
@@ -249,7 +248,7 @@ export function TableDataViewer() {
|
||||
onNextPage={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
/>
|
||||
|
||||
<TableRowModal
|
||||
<RowModal
|
||||
mode='add'
|
||||
isOpen={showAddModal}
|
||||
onClose={() => setShowAddModal(false)}
|
||||
@@ -261,7 +260,7 @@ export function TableDataViewer() {
|
||||
/>
|
||||
|
||||
{editingRow && (
|
||||
<TableRowModal
|
||||
<RowModal
|
||||
mode='edit'
|
||||
isOpen={true}
|
||||
onClose={() => setEditingRow(null)}
|
||||
@@ -275,7 +274,7 @@ export function TableDataViewer() {
|
||||
)}
|
||||
|
||||
{deletingRows.length > 0 && (
|
||||
<TableRowModal
|
||||
<RowModal
|
||||
mode='delete'
|
||||
isOpen={true}
|
||||
onClose={() => setDeletingRows([])}
|
||||
@@ -289,7 +288,7 @@ export function TableDataViewer() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SchemaViewerModal
|
||||
<SchemaModal
|
||||
isOpen={showSchemaModal}
|
||||
onClose={() => setShowSchemaModal(false)}
|
||||
columns={columns}
|
||||
@@ -302,7 +301,7 @@ export function TableDataViewer() {
|
||||
copied={copied}
|
||||
/>
|
||||
|
||||
<RowContextMenu
|
||||
<ContextMenu
|
||||
contextMenu={contextMenu}
|
||||
onClose={closeContextMenu}
|
||||
onEdit={handleContextMenuEdit}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { TableRow } from '@/lib/table'
|
||||
import type { ContextMenuState } from '../types'
|
||||
import type { ContextMenuState } from '../lib/types'
|
||||
|
||||
interface UseContextMenuReturn {
|
||||
contextMenu: ContextMenuState
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { TableDefinition, TableRow } from '@/lib/table'
|
||||
import type { BuilderQueryResult } from '../../components/table-query-builder'
|
||||
import { ROWS_PER_PAGE } from '../constants'
|
||||
import { ROWS_PER_PAGE } from '../lib/constants'
|
||||
import type { QueryOptions } from '../lib/types'
|
||||
|
||||
interface UseTableDataParams {
|
||||
workspaceId: string
|
||||
tableId: string
|
||||
queryOptions: BuilderQueryResult
|
||||
queryOptions: QueryOptions
|
||||
currentPage: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './constants'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Filter, Sort, TableRow } from '@/lib/table'
|
||||
|
||||
/**
|
||||
* Query options for filtering and sorting table data
|
||||
*/
|
||||
export interface QueryOptions {
|
||||
filter: Filter | null
|
||||
sort: Sort | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Data for viewing a cell's full content in a modal
|
||||
*/
|
||||
export interface CellViewerData {
|
||||
columnName: string
|
||||
value: unknown
|
||||
type: 'json' | 'text' | 'date' | 'boolean' | 'number'
|
||||
}
|
||||
|
||||
/**
|
||||
* State for the row context menu (right-click)
|
||||
*/
|
||||
export interface ContextMenuState {
|
||||
isOpen: boolean
|
||||
position: { x: number; y: number }
|
||||
row: TableRow | null
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
export function getTypeBadgeVariant(
|
||||
type: string
|
||||
): 'green' | 'blue' | 'purple' | 'orange' | 'teal' | 'gray' {
|
||||
type BadgeVariant = 'green' | 'blue' | 'purple' | 'orange' | 'teal' | 'gray'
|
||||
|
||||
/**
|
||||
* Returns the appropriate badge color variant for a column type
|
||||
*/
|
||||
export function getTypeBadgeVariant(type: string): BadgeVariant {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return 'green'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TableDataViewer } from './table-data-viewer'
|
||||
import { TableViewer } from './components'
|
||||
|
||||
export default function TablePage() {
|
||||
return <TableDataViewer />
|
||||
return <TableViewer />
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from './cell-viewer-modal'
|
||||
export * from './row-context-menu'
|
||||
export * from './schema-viewer-modal'
|
||||
export * from './table-body-states'
|
||||
export * from './table-cell-renderer'
|
||||
export * from './table-header-bar'
|
||||
export * from './table-pagination'
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './table-data-viewer'
|
||||
export * from './types'
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { TableRow } from '@/lib/table'
|
||||
|
||||
export interface CellViewerData {
|
||||
columnName: string
|
||||
value: unknown
|
||||
type: 'json' | 'text' | 'date' | 'boolean' | 'number'
|
||||
}
|
||||
|
||||
export interface ContextMenuState {
|
||||
isOpen: boolean
|
||||
position: { x: number; y: number }
|
||||
row: TableRow | null
|
||||
}
|
||||
Reference in New Issue
Block a user