mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(tables): consolidation
This commit is contained in:
@@ -31,14 +31,14 @@ export function ResourceOptionsBar({
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
{search && (
|
||||
<div className='relative flex-1'>
|
||||
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-0 h-[14px] w-[14px] text-[var(--text-muted)]' />
|
||||
<div className='flex flex-1 items-center'>
|
||||
<Search className='pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-muted)]' />
|
||||
<input
|
||||
type='text'
|
||||
value={search.value}
|
||||
onChange={(e) => search.onChange(e.target.value)}
|
||||
placeholder={search.placeholder ?? 'Search...'}
|
||||
className='w-full bg-transparent py-[4px] pl-[24px] font-base text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
|
||||
className='w-full bg-transparent py-[4px] pl-[10px] text-[12px] text-[var(--text-secondary)] outline-none placeholder:text-[var(--text-subtle)]'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Trash2, X } from 'lucide-react'
|
||||
import { Button } from '@/components/emcn'
|
||||
|
||||
interface ActionBarProps {
|
||||
selectedCount: number
|
||||
onDelete: () => void
|
||||
onClearSelection: () => void
|
||||
}
|
||||
|
||||
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]'>
|
||||
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
|
||||
{selectedCount} {selectedCount === 1 ? 'row' : 'rows'} selected
|
||||
</span>
|
||||
<Button variant='ghost' size='sm' onClick={onClearSelection}>
|
||||
<X className='mr-[4px] h-[10px] w-[10px]' />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant='destructive' size='sm' onClick={onDelete}>
|
||||
<Trash2 className='mr-[4px] h-[10px] w-[10px]' />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { ActionBar } from './action-bar'
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button, TableCell, TableRow } from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
|
||||
interface LoadingRowsProps {
|
||||
columns: ColumnDefinition[]
|
||||
}
|
||||
|
||||
export function LoadingRows({ columns }: LoadingRowsProps) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: 25 }).map((_, rowIndex) => (
|
||||
<TableRow key={rowIndex}>
|
||||
<TableCell>
|
||||
<Skeleton className='h-[14px] w-[14px]' />
|
||||
</TableCell>
|
||||
{columns.map((col, colIndex) => {
|
||||
const baseWidth =
|
||||
col.type === 'json'
|
||||
? 200
|
||||
: col.type === 'string'
|
||||
? 160
|
||||
: col.type === 'number'
|
||||
? 80
|
||||
: col.type === 'boolean'
|
||||
? 50
|
||||
: col.type === 'date'
|
||||
? 100
|
||||
: 120
|
||||
const variation = ((rowIndex + colIndex) % 3) * 20
|
||||
const width = baseWidth + variation
|
||||
|
||||
return (
|
||||
<TableCell key={col.name}>
|
||||
<Skeleton className='h-[16px]' style={{ width: `${width}px` }} />
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface EmptyRowsProps {
|
||||
columnCount: number
|
||||
hasFilter: boolean
|
||||
onAddRow: () => void
|
||||
}
|
||||
|
||||
export function EmptyRows({ columnCount, hasFilter, onAddRow }: EmptyRowsProps) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columnCount + 1} className='h-[160px]'>
|
||||
<div
|
||||
className='-translate-x-1/2 fixed'
|
||||
style={{ left: 'calc(50% + var(--sidebar-width, 0px) / 2)' }}
|
||||
>
|
||||
<div className='flex flex-col items-center gap-[12px]'>
|
||||
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
||||
{hasFilter ? 'No rows match your filter' : 'No data'}
|
||||
</span>
|
||||
{!hasFilter && (
|
||||
<Button variant='default' size='sm' onClick={onAddRow}>
|
||||
<Plus className='mr-[4px] h-[12px] w-[12px]' />
|
||||
Add first row
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { EmptyRows, LoadingRows } from './body-states'
|
||||
@@ -1,178 +0,0 @@
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
import { STRING_TRUNCATE_LENGTH } from '../../constants'
|
||||
import type { CellViewerData } from '../../types'
|
||||
import { InlineCellEditor } from '../inline-cell-editor'
|
||||
|
||||
interface CellRendererProps {
|
||||
value: unknown
|
||||
column: ColumnDefinition
|
||||
isEditing: boolean
|
||||
onCellClick: (columnName: string, value: unknown, type: CellViewerData['type']) => void
|
||||
onDoubleClick: () => void
|
||||
onSave: (value: unknown) => void
|
||||
onCancel: () => void
|
||||
onBooleanToggle: () => void
|
||||
}
|
||||
|
||||
export function CellRenderer({
|
||||
value,
|
||||
column,
|
||||
isEditing,
|
||||
onCellClick,
|
||||
onDoubleClick,
|
||||
onSave,
|
||||
onCancel,
|
||||
onBooleanToggle,
|
||||
}: CellRendererProps) {
|
||||
if (isEditing) {
|
||||
return <InlineCellEditor value={value} column={column} onSave={onSave} onCancel={onCancel} />
|
||||
}
|
||||
|
||||
const isNull = value === null || value === undefined
|
||||
|
||||
if (column.type === 'boolean') {
|
||||
const boolValue = Boolean(value)
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='cursor-pointer select-none'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onBooleanToggle()
|
||||
}}
|
||||
>
|
||||
<span className={boolValue ? 'text-green-500' : 'text-[var(--text-tertiary)]'}>
|
||||
{isNull ? (
|
||||
<span className='text-[var(--text-muted)] italic'>—</span>
|
||||
) : boolValue ? (
|
||||
'true'
|
||||
) : (
|
||||
'false'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (isNull) {
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[var(--text-muted)] italic'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'json') {
|
||||
const jsonStr = JSON.stringify(value)
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='block max-w-[300px] cursor-pointer select-none truncate rounded-[4px] border border-[var(--border-1)] px-[6px] py-[2px] text-left font-mono text-[11px] text-[var(--text-secondary)] transition-colors hover:border-[var(--text-muted)] hover:text-[var(--text-primary)]'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onCellClick(column.name, value, 'json')
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
title='Click to view, double-click to edit'
|
||||
>
|
||||
{jsonStr}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'number') {
|
||||
return (
|
||||
<span
|
||||
className='cursor-text font-mono text-[12px] text-[var(--text-secondary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{String(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'date') {
|
||||
try {
|
||||
const date = new Date(String(value))
|
||||
const formatted = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[12px] text-[var(--text-secondary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{formatted}
|
||||
</span>
|
||||
)
|
||||
} catch {
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[var(--text-primary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{String(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const strValue = String(value)
|
||||
if (strValue.length > STRING_TRUNCATE_LENGTH) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='block max-w-[300px] cursor-pointer select-none truncate text-left text-[var(--text-primary)] underline decoration-[var(--border-1)] decoration-dotted underline-offset-2 transition-colors hover:decoration-[var(--text-muted)]'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onCellClick(column.name, value, 'text')
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
title='Click to view, double-click to edit'
|
||||
>
|
||||
{strValue}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[var(--text-primary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{strValue}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { CellRenderer } from './cell-renderer'
|
||||
@@ -1,12 +1,6 @@
|
||||
export * from './action-bar'
|
||||
export * from './body-states'
|
||||
export * from './cell-renderer'
|
||||
export * from './cell-viewer-modal'
|
||||
export * from './context-menu'
|
||||
export * from './inline-cell-editor'
|
||||
export * from './pagination'
|
||||
export * from './query-builder'
|
||||
export * from './row-modal'
|
||||
export * from './schema-modal'
|
||||
export * from './table'
|
||||
export * from './table-row-cells'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { InlineCellEditor } from './inline-cell-editor'
|
||||
@@ -1,63 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ColumnDefinition } from '@/lib/table'
|
||||
import { cleanCellValue, formatValueForInput } from '../../utils'
|
||||
|
||||
interface InlineCellEditorProps {
|
||||
value: unknown
|
||||
column: ColumnDefinition
|
||||
onSave: (value: unknown) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function InlineCellEditor({ value, column, onSave, onCancel }: InlineCellEditorProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [draft, setDraft] = useState(() => formatValueForInput(value, column.type))
|
||||
const doneRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const input = inputRef.current
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSave = () => {
|
||||
if (doneRef.current) return
|
||||
doneRef.current = true
|
||||
|
||||
try {
|
||||
const cleaned = cleanCellValue(draft, column)
|
||||
onSave(cleaned)
|
||||
} catch {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
doneRef.current = true
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const inputType = column.type === 'number' ? 'number' : column.type === 'date' ? 'date' : 'text'
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={inputType}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
className='h-full w-full rounded-[2px] border-none bg-transparent px-[4px] py-[2px] text-[13px] text-[var(--text-primary)] outline-none ring-1 ring-[var(--accent)] ring-inset'
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { Pagination } from './pagination'
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Button } from '@/components/emcn'
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
totalCount: number
|
||||
onPreviousPage: () => void
|
||||
onNextPage: () => void
|
||||
}
|
||||
|
||||
export function Pagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalCount,
|
||||
onPreviousPage,
|
||||
onNextPage,
|
||||
}: PaginationProps) {
|
||||
if (totalPages <= 1) return null
|
||||
|
||||
return (
|
||||
<div className='flex h-[40px] shrink-0 items-center justify-between border-[var(--border)] border-t px-[16px]'>
|
||||
<span className='text-[11px] text-[var(--text-tertiary)]'>
|
||||
Page {currentPage + 1} of {totalPages} ({totalCount} rows)
|
||||
</span>
|
||||
<div className='flex items-center gap-[4px]'>
|
||||
<Button variant='ghost' size='sm' onClick={onPreviousPage} disabled={currentPage === 0}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={onNextPage}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { TableRowCells } from './table-row-cells'
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Checkbox, TableCell, TableRow } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { ColumnDefinition, TableRow as TableRowType } from '@/lib/table'
|
||||
import type { CellViewerData } from '../../types'
|
||||
import { CellRenderer } from '../cell-renderer'
|
||||
|
||||
interface TableRowCellsProps {
|
||||
row: TableRowType
|
||||
columns: ColumnDefinition[]
|
||||
isSelected: boolean
|
||||
editingColumnName: string | null
|
||||
onCellClick: (columnName: string, value: unknown, type: CellViewerData['type']) => void
|
||||
onDoubleClick: (rowId: string, columnName: string) => void
|
||||
onSave: (rowId: string, columnName: string, value: unknown) => void
|
||||
onCancel: () => void
|
||||
onBooleanToggle: (rowId: string, columnName: string, currentValue: boolean) => void
|
||||
onContextMenu: (e: React.MouseEvent, row: TableRowType) => void
|
||||
onSelectRow: (rowId: string) => void
|
||||
}
|
||||
|
||||
export const TableRowCells = React.memo(function TableRowCells({
|
||||
row,
|
||||
columns,
|
||||
isSelected,
|
||||
editingColumnName,
|
||||
onCellClick,
|
||||
onDoubleClick,
|
||||
onSave,
|
||||
onCancel,
|
||||
onBooleanToggle,
|
||||
onContextMenu,
|
||||
onSelectRow,
|
||||
}: TableRowCellsProps) {
|
||||
return (
|
||||
<TableRow
|
||||
className={cn('group hover:bg-[var(--surface-4)]', isSelected && 'bg-[var(--surface-5)]')}
|
||||
onContextMenu={(e) => onContextMenu(e, row)}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox size='sm' checked={isSelected} onCheckedChange={() => onSelectRow(row.id)} />
|
||||
</TableCell>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={column.name}>
|
||||
<div className='max-w-[300px] truncate text-[13px]'>
|
||||
<CellRenderer
|
||||
value={row.data[column.name]}
|
||||
column={column}
|
||||
isEditing={editingColumnName === column.name}
|
||||
onCellClick={onCellClick}
|
||||
onDoubleClick={() => onDoubleClick(row.id, column.name)}
|
||||
onSave={(value) => onSave(row.id, column.name, value)}
|
||||
onCancel={onCancel}
|
||||
onBooleanToggle={() =>
|
||||
onBooleanToggle(row.id, column.name, Boolean(row.data[column.name]))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
@@ -1,30 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Button, Checkbox, Skeleton } from '@/components/emcn'
|
||||
import {
|
||||
Badge,
|
||||
Checkbox,
|
||||
Table as EmcnTable,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/emcn'
|
||||
import { Table as TableIcon } from '@/components/emcn/icons'
|
||||
import type { TableRow as TableRowType } from '@/lib/table'
|
||||
Calendar as CalendarIcon,
|
||||
Table as TableIcon,
|
||||
TypeBoolean,
|
||||
TypeJson,
|
||||
TypeNumber,
|
||||
TypeText,
|
||||
} from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { ColumnDefinition, TableRow as TableRowType } from '@/lib/table'
|
||||
import { ResourceHeader, ResourceOptionsBar } from '@/app/workspace/[workspaceId]/components'
|
||||
import { useUpdateTableRow } from '@/hooks/queries/tables'
|
||||
import { useCreateTableRow, useUpdateTableRow } from '@/hooks/queries/tables'
|
||||
import { STRING_TRUNCATE_LENGTH } from '../../constants'
|
||||
import { useContextMenu, useRowSelection, useTableData } from '../../hooks'
|
||||
import type { CellViewerData, EditingCell, QueryOptions } from '../../types'
|
||||
import { ActionBar } from '../action-bar'
|
||||
import { EmptyRows, LoadingRows } from '../body-states'
|
||||
import { cleanCellValue, formatValueForInput } from '../../utils'
|
||||
import { CellViewerModal } from '../cell-viewer-modal'
|
||||
import { ContextMenu } from '../context-menu'
|
||||
import { Pagination } from '../pagination'
|
||||
import { RowModal } from '../row-modal'
|
||||
import { SchemaModal } from '../schema-modal'
|
||||
import { TableRowCells } from '../table-row-cells'
|
||||
|
||||
const EMPTY_COLUMNS: never[] = []
|
||||
|
||||
@@ -47,6 +45,11 @@ export function Table() {
|
||||
const [showSchemaModal, setShowSchemaModal] = useState(false)
|
||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null)
|
||||
|
||||
const [editingEmptyCell, setEditingEmptyCell] = useState<{
|
||||
rowIndex: number
|
||||
columnName: string
|
||||
} | null>(null)
|
||||
|
||||
const [cellViewer, setCellViewer] = useState<CellViewerData | null>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
@@ -66,6 +69,7 @@ export function Table() {
|
||||
} = useContextMenu()
|
||||
|
||||
const updateRowMutation = useUpdateTableRow({ workspaceId, tableId })
|
||||
const createRowMutation = useCreateTableRow({ workspaceId, tableId })
|
||||
|
||||
const columns = useMemo(
|
||||
() => tableData?.schema?.columns || EMPTY_COLUMNS,
|
||||
@@ -190,6 +194,27 @@ export function Table() {
|
||||
[]
|
||||
)
|
||||
|
||||
const handleEmptyRowDoubleClick = useCallback((rowIndex: number, columnName: string) => {
|
||||
const column = columnsRef.current.find((c) => c.name === columnName)
|
||||
if (!column || column.type === 'json' || column.type === 'boolean') return
|
||||
setEditingEmptyCell({ rowIndex, columnName })
|
||||
}, [])
|
||||
|
||||
const createRef = useRef(createRowMutation.mutate)
|
||||
useEffect(() => {
|
||||
createRef.current = createRowMutation.mutate
|
||||
}, [createRowMutation.mutate])
|
||||
|
||||
const handleEmptyRowSave = useCallback((columnName: string, value: unknown) => {
|
||||
setEditingEmptyCell(null)
|
||||
if (value === null || value === undefined || value === '') return
|
||||
createRef.current({ [columnName]: value })
|
||||
}, [])
|
||||
|
||||
const handleEmptyRowCancel = useCallback(() => {
|
||||
setEditingEmptyCell(null)
|
||||
}, [])
|
||||
|
||||
if (isLoadingTable) {
|
||||
return (
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
@@ -215,68 +240,71 @@ export function Table() {
|
||||
|
||||
<ResourceOptionsBar onSort={handleSort} onFilter={handleFilter} />
|
||||
|
||||
{hasSelection && (
|
||||
<ActionBar
|
||||
selectedCount={selectedCount}
|
||||
onDelete={handleDeleteSelected}
|
||||
onClearSelection={clearSelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='flex-1 overflow-auto'>
|
||||
<EmcnTable>
|
||||
<TableHeader className='sticky top-0 z-10 bg-[var(--surface-3)]'>
|
||||
<TableRow>
|
||||
<TableHead className='w-[40px]'>
|
||||
<div className='min-h-0 flex-1 overflow-auto overscroll-none'>
|
||||
<table className='border-collapse text-[13px]'>
|
||||
<colgroup>
|
||||
<col className='w-[40px]' />
|
||||
{columns.map((col) => (
|
||||
<col key={col.name} className='w-[160px]' />
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className='sticky top-0 z-10 bg-white shadow-[inset_0_-1px_0_var(--border)] dark:bg-[var(--bg)]'>
|
||||
<tr>
|
||||
<th className='border-[var(--border)] border-r py-[10px] pr-[12px] pl-[24px] text-left align-middle'>
|
||||
<Checkbox size='sm' checked={isAllSelected} onCheckedChange={handleSelectAll} />
|
||||
</TableHead>
|
||||
</th>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column.name}>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<span className='text-[12px]'>{column.name}</span>
|
||||
<Badge variant='outline' size='sm'>
|
||||
{column.type}
|
||||
</Badge>
|
||||
{column.required && (
|
||||
<span className='text-[10px] text-[var(--text-error)]'>*</span>
|
||||
)}
|
||||
<th
|
||||
key={column.name}
|
||||
className='border-[var(--border)] border-r px-[24px] py-[10px] text-left align-middle'
|
||||
>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<ColumnTypeIcon type={column.type} />
|
||||
<span className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
{column.name}
|
||||
</span>
|
||||
</div>
|
||||
</TableHead>
|
||||
</th>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoadingRows ? (
|
||||
<LoadingRows columns={columns} />
|
||||
) : rows.length === 0 ? (
|
||||
<EmptyRows
|
||||
columnCount={columns.length}
|
||||
hasFilter={!!queryOptions.filter}
|
||||
onAddRow={handleAddRow}
|
||||
/>
|
||||
<TableSkeleton columns={columns} />
|
||||
) : (
|
||||
rows.map((row) => (
|
||||
<TableRowCells
|
||||
key={row.id}
|
||||
row={row}
|
||||
<>
|
||||
{rows.map((row) => (
|
||||
<DataRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
columns={columns}
|
||||
isSelected={selectedRows.has(row.id)}
|
||||
editingColumnName={
|
||||
editingCell?.rowId === row.id ? editingCell.columnName : null
|
||||
}
|
||||
onCellClick={handleCellClick}
|
||||
onDoubleClick={handleCellDoubleClick}
|
||||
onSave={handleInlineSave}
|
||||
onCancel={handleInlineCancel}
|
||||
onBooleanToggle={handleBooleanToggle}
|
||||
onContextMenu={handleRowContextMenu}
|
||||
onSelectRow={handleSelectRow}
|
||||
/>
|
||||
))}
|
||||
<PlaceholderRows
|
||||
columns={columns}
|
||||
isSelected={selectedRows.has(row.id)}
|
||||
editingColumnName={editingCell?.rowId === row.id ? editingCell.columnName : null}
|
||||
onCellClick={handleCellClick}
|
||||
onDoubleClick={handleCellDoubleClick}
|
||||
onSave={handleInlineSave}
|
||||
onCancel={handleInlineCancel}
|
||||
onBooleanToggle={handleBooleanToggle}
|
||||
onContextMenu={handleRowContextMenu}
|
||||
onSelectRow={handleSelectRow}
|
||||
editingEmptyCell={editingEmptyCell}
|
||||
onDoubleClick={handleEmptyRowDoubleClick}
|
||||
onSave={handleEmptyRowSave}
|
||||
onCancel={handleEmptyRowCancel}
|
||||
/>
|
||||
))
|
||||
</>
|
||||
)}
|
||||
</TableBody>
|
||||
</EmcnTable>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
<TablePagination
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
@@ -346,3 +374,432 @@ export function Table() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DataRow = React.memo(function DataRow({
|
||||
row,
|
||||
columns,
|
||||
isSelected,
|
||||
editingColumnName,
|
||||
onCellClick,
|
||||
onDoubleClick,
|
||||
onSave,
|
||||
onCancel,
|
||||
onBooleanToggle,
|
||||
onContextMenu,
|
||||
onSelectRow,
|
||||
}: {
|
||||
row: TableRowType
|
||||
columns: ColumnDefinition[]
|
||||
isSelected: boolean
|
||||
editingColumnName: string | null
|
||||
onCellClick: (columnName: string, value: unknown, type: CellViewerData['type']) => void
|
||||
onDoubleClick: (rowId: string, columnName: string) => void
|
||||
onSave: (rowId: string, columnName: string, value: unknown) => void
|
||||
onCancel: () => void
|
||||
onBooleanToggle: (rowId: string, columnName: string, currentValue: boolean) => void
|
||||
onContextMenu: (e: React.MouseEvent, row: TableRowType) => void
|
||||
onSelectRow: (rowId: string) => void
|
||||
}) {
|
||||
return (
|
||||
<tr
|
||||
className={cn('group', isSelected && 'bg-[var(--surface-5)]')}
|
||||
onContextMenu={(e) => onContextMenu(e, row)}
|
||||
>
|
||||
<td className='border-[var(--border)] border-r border-b py-[10px] pr-[12px] pl-[24px] align-middle'>
|
||||
<Checkbox size='sm' checked={isSelected} onCheckedChange={() => onSelectRow(row.id)} />
|
||||
</td>
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={column.name}
|
||||
className='border-[var(--border)] border-r border-b px-[24px] py-[10px] align-middle'
|
||||
>
|
||||
<div className='max-w-[300px] truncate text-[13px]'>
|
||||
<CellContent
|
||||
value={row.data[column.name]}
|
||||
column={column}
|
||||
isEditing={editingColumnName === column.name}
|
||||
onCellClick={onCellClick}
|
||||
onDoubleClick={() => onDoubleClick(row.id, column.name)}
|
||||
onSave={(value) => onSave(row.id, column.name, value)}
|
||||
onCancel={onCancel}
|
||||
onBooleanToggle={() =>
|
||||
onBooleanToggle(row.id, column.name, Boolean(row.data[column.name]))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
function CellContent({
|
||||
value,
|
||||
column,
|
||||
isEditing,
|
||||
onCellClick,
|
||||
onDoubleClick,
|
||||
onSave,
|
||||
onCancel,
|
||||
onBooleanToggle,
|
||||
}: {
|
||||
value: unknown
|
||||
column: ColumnDefinition
|
||||
isEditing: boolean
|
||||
onCellClick: (columnName: string, value: unknown, type: CellViewerData['type']) => void
|
||||
onDoubleClick: () => void
|
||||
onSave: (value: unknown) => void
|
||||
onCancel: () => void
|
||||
onBooleanToggle: () => void
|
||||
}) {
|
||||
if (isEditing) {
|
||||
return <InlineEditor value={value} column={column} onSave={onSave} onCancel={onCancel} />
|
||||
}
|
||||
|
||||
const isNull = value === null || value === undefined
|
||||
|
||||
if (column.type === 'boolean') {
|
||||
const boolValue = Boolean(value)
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='cursor-pointer select-none'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onBooleanToggle()
|
||||
}}
|
||||
>
|
||||
<span className={boolValue ? 'text-green-500' : 'text-[var(--text-tertiary)]'}>
|
||||
{isNull ? (
|
||||
<span className='text-[var(--text-muted)] italic'>—</span>
|
||||
) : boolValue ? (
|
||||
'true'
|
||||
) : (
|
||||
'false'
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (isNull) {
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[var(--text-muted)] italic'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'json') {
|
||||
const jsonStr = JSON.stringify(value)
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='block max-w-[300px] cursor-pointer select-none truncate rounded-[4px] border border-[var(--border-1)] px-[6px] py-[2px] text-left font-mono text-[11px] text-[var(--text-secondary)] transition-colors hover:border-[var(--text-muted)] hover:text-[var(--text-primary)]'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onCellClick(column.name, value, 'json')
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
title='Click to view, double-click to edit'
|
||||
>
|
||||
{jsonStr}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'number') {
|
||||
return (
|
||||
<span
|
||||
className='cursor-text font-mono text-[12px] text-[var(--text-secondary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{String(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (column.type === 'date') {
|
||||
try {
|
||||
const date = new Date(String(value))
|
||||
const formatted = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[12px] text-[var(--text-secondary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{formatted}
|
||||
</span>
|
||||
)
|
||||
} catch {
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[var(--text-primary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{String(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const strValue = String(value)
|
||||
if (strValue.length > STRING_TRUNCATE_LENGTH) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='block max-w-[300px] cursor-pointer select-none truncate text-left text-[var(--text-primary)] underline decoration-[var(--border-1)] decoration-dotted underline-offset-2 transition-colors hover:decoration-[var(--text-muted)]'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onCellClick(column.name, value, 'text')
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
title='Click to view, double-click to edit'
|
||||
>
|
||||
{strValue}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className='cursor-text text-[var(--text-primary)]'
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDoubleClick()
|
||||
}}
|
||||
>
|
||||
{strValue}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function InlineEditor({
|
||||
value,
|
||||
column,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: {
|
||||
value: unknown
|
||||
column: ColumnDefinition
|
||||
onSave: (value: unknown) => void
|
||||
onCancel: () => void
|
||||
}) {
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const [draft, setDraft] = useState(() => formatValueForInput(value, column.type))
|
||||
const doneRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const input = inputRef.current
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleSave = () => {
|
||||
if (doneRef.current) return
|
||||
doneRef.current = true
|
||||
|
||||
try {
|
||||
const cleaned = cleanCellValue(draft, column)
|
||||
onSave(cleaned)
|
||||
} catch {
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleSave()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
doneRef.current = true
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
const inputType = column.type === 'number' ? 'number' : column.type === 'date' ? 'date' : 'text'
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={inputType}
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleSave}
|
||||
className='h-full w-full rounded-[2px] border-none bg-transparent px-[4px] py-[2px] text-[13px] text-[var(--text-primary)] outline-none ring-1 ring-[var(--accent)] ring-inset'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TablePagination({
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalCount,
|
||||
onPreviousPage,
|
||||
onNextPage,
|
||||
}: {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
totalCount: number
|
||||
onPreviousPage: () => void
|
||||
onNextPage: () => void
|
||||
}) {
|
||||
if (totalPages <= 1) return null
|
||||
|
||||
return (
|
||||
<div className='flex h-[40px] shrink-0 items-center justify-between border-[var(--border)] border-t px-[16px]'>
|
||||
<span className='text-[11px] text-[var(--text-tertiary)]'>
|
||||
Page {currentPage + 1} of {totalPages} ({totalCount} rows)
|
||||
</span>
|
||||
<div className='flex items-center gap-[4px]'>
|
||||
<Button variant='ghost' size='sm' onClick={onPreviousPage} disabled={currentPage === 0}>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={onNextPage}
|
||||
disabled={currentPage === totalPages - 1}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableSkeleton({ columns }: { columns: ColumnDefinition[] }) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: 25 }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
<td className='border-[var(--border)] border-r border-b py-[10px] pr-[12px] pl-[24px] align-middle'>
|
||||
<Skeleton className='h-[14px] w-[14px]' />
|
||||
</td>
|
||||
{columns.map((col, colIndex) => {
|
||||
const baseWidth =
|
||||
col.type === 'json'
|
||||
? 200
|
||||
: col.type === 'string'
|
||||
? 160
|
||||
: col.type === 'number'
|
||||
? 80
|
||||
: col.type === 'boolean'
|
||||
? 50
|
||||
: col.type === 'date'
|
||||
? 100
|
||||
: 120
|
||||
const variation = ((rowIndex + colIndex) % 3) * 20
|
||||
const width = baseWidth + variation
|
||||
|
||||
return (
|
||||
<td
|
||||
key={col.name}
|
||||
className='border-[var(--border)] border-r border-b px-[24px] py-[10px] align-middle'
|
||||
>
|
||||
<Skeleton className='h-[16px]' style={{ width: `${width}px` }} />
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const PLACEHOLDER_ROW_COUNT = 50
|
||||
|
||||
function PlaceholderRows({
|
||||
columns,
|
||||
editingEmptyCell,
|
||||
onDoubleClick,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: {
|
||||
columns: ColumnDefinition[]
|
||||
editingEmptyCell: { rowIndex: number; columnName: string } | null
|
||||
onDoubleClick: (rowIndex: number, columnName: string) => void
|
||||
onSave: (columnName: string, value: unknown) => void
|
||||
onCancel: () => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: PLACEHOLDER_ROW_COUNT }).map((_, i) => (
|
||||
<tr key={`placeholder-${i}`}>
|
||||
<td className='border-[var(--border)] border-r border-b py-[10px] pr-[12px] pl-[24px] align-middle' />
|
||||
{columns.map((col) => {
|
||||
const isEditing =
|
||||
editingEmptyCell?.rowIndex === i && editingEmptyCell.columnName === col.name
|
||||
|
||||
return (
|
||||
<td
|
||||
key={col.name}
|
||||
className='border-[var(--border)] border-r border-b px-[24px] py-[10px] align-middle'
|
||||
onDoubleClick={() => onDoubleClick(i, col.name)}
|
||||
>
|
||||
{isEditing ? (
|
||||
<InlineEditor
|
||||
value={null}
|
||||
column={col}
|
||||
onSave={(value) => onSave(col.name, value)}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
) : (
|
||||
<div className='min-h-[20px]' />
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const COLUMN_TYPE_ICONS: Record<string, React.ElementType> = {
|
||||
string: TypeText,
|
||||
number: TypeNumber,
|
||||
boolean: TypeBoolean,
|
||||
date: CalendarIcon,
|
||||
json: TypeJson,
|
||||
}
|
||||
|
||||
function ColumnTypeIcon({ type }: { type: string }) {
|
||||
const Icon = COLUMN_TYPE_ICONS[type] ?? TypeText
|
||||
return <Icon className='h-[14px] w-[14px] shrink-0 text-[var(--text-muted)]' />
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ import { cn } from '@/lib/core/utils/cn'
|
||||
*/
|
||||
const checkboxVariants = cva(
|
||||
[
|
||||
'peer shrink-0 cursor-pointer rounded-[4px] border transition-colors',
|
||||
'peer flex shrink-0 cursor-pointer items-center justify-center rounded-[4px] border transition-colors',
|
||||
'border-[var(--border-1)] bg-transparent',
|
||||
'focus-visible:outline-none',
|
||||
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
|
||||
'data-[state=checked]:border-[var(--brand-tertiary-2)] data-[state=checked]:bg-[var(--brand-tertiary-2)]',
|
||||
'data-[state=checked]:border-[var(--text-primary)] data-[state=checked]:bg-[var(--text-primary)]',
|
||||
].join(' '),
|
||||
{
|
||||
variants: {
|
||||
@@ -89,7 +89,7 @@ const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root
|
||||
className={cn(checkboxVariants({ size }), className)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className='flex items-center justify-center text-[var(--white)]'>
|
||||
<CheckboxPrimitive.Indicator className='flex items-center justify-center text-[var(--surface-2)]'>
|
||||
<Check className={cn(checkboxIconVariants({ size }))} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
|
||||
@@ -39,6 +39,10 @@ export { Table } from './table'
|
||||
export { TerminalWindow } from './terminal-window'
|
||||
export { Trash } from './trash'
|
||||
export { Trash2 } from './trash2'
|
||||
export { TypeBoolean } from './type-boolean'
|
||||
export { TypeJson } from './type-json'
|
||||
export { TypeNumber } from './type-number'
|
||||
export { TypeText } from './type-text'
|
||||
export { Undo } from './undo'
|
||||
export { Wrap } from './wrap'
|
||||
export { ZoomIn } from './zoom-in'
|
||||
|
||||
25
apps/sim/components/emcn/icons/type-boolean.tsx
Normal file
25
apps/sim/components/emcn/icons/type-boolean.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* Type boolean icon component - toggle switch for boolean columns
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function TypeBoolean(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}
|
||||
>
|
||||
<rect x='1.25' y='5.25' width='18' height='10.5' rx='5.25' />
|
||||
<circle cx='6.5' cy='10.5' r='3' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
25
apps/sim/components/emcn/icons/type-json.tsx
Normal file
25
apps/sim/components/emcn/icons/type-json.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* Type JSON icon component - curly braces for JSON columns
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function TypeJson(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='M5.75 1.25C3.54086 1.25 1.75 3.04086 1.75 5.25V7.75C1.75 9.13071 0.630712 10.25 -0.75 10.25C0.630712 10.25 1.75 11.3693 1.75 12.75V15.25C1.75 17.4591 3.54086 19.25 5.75 19.25' />
|
||||
<path d='M14.75 1.25C16.9591 1.25 18.75 3.04086 18.75 5.25V7.75C18.75 9.13071 19.8693 10.25 21.25 10.25C19.8693 10.25 18.75 11.3693 18.75 12.75V15.25C18.75 17.4591 16.9591 19.25 14.75 19.25' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
27
apps/sim/components/emcn/icons/type-number.tsx
Normal file
27
apps/sim/components/emcn/icons/type-number.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* Type number icon component - hash symbol for number columns
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function TypeNumber(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='M3.25 7.25H17.75' />
|
||||
<path d='M2.75 13.75H17.25' />
|
||||
<path d='M8.25 1.25L6.25 19.75' />
|
||||
<path d='M14.25 1.25L12.25 19.75' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
26
apps/sim/components/emcn/icons/type-text.tsx
Normal file
26
apps/sim/components/emcn/icons/type-text.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { SVGProps } from 'react'
|
||||
|
||||
/**
|
||||
* Type text icon component - letter T for string columns
|
||||
* @param props - SVG properties including className, fill, etc.
|
||||
*/
|
||||
export function TypeText(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='M3.25 2.25H17.25' />
|
||||
<path d='M10.25 2.25V18.75' />
|
||||
<path d='M7.25 18.75H13.25' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user