Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions web/default/src/components/data-table/core/badge-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
Copyright (C) 2023-2026 QuantumNous

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { cn } from '@/lib/utils'

type BadgeCellProps = React.HTMLAttributes<HTMLDivElement>

export function BadgeCell({ className, ...props }: BadgeCellProps) {
return (
<div
className={cn(
'-ml-1.5 flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
className
)}
{...props}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { StatusBadgeList } from '@/components/status-badge'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { StatusBadgeList } from '@/components/status-badge'

interface BadgeListCellProps {
items: React.ReactNode[]
Expand All @@ -50,7 +50,7 @@ export function BadgeListCell({
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<div className='-ml-1.5' />}>
<TooltipTrigger render={<div className='-ml-1.5 max-w-full' />}>
<StatusBadgeList
items={items}
max={max}
Expand Down
36 changes: 33 additions & 3 deletions web/default/src/components/data-table/core/data-table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { flexRender, type Row } from '@tanstack/react-table'
import { flexRender, type Cell, type Row } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
import { TableCell, TableRow } from '@/components/ui/table'
import { TruncatedCell } from './truncated-cell'
import type { DataTableColumnClassName } from './types'

type DataTableRowProps<TData> = {
Expand All @@ -42,9 +44,12 @@ function DataTableRowInner<TData>({
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={getColumnClassName?.(cell.column.id, 'cell')}
className={cn(
'max-w-full min-w-0 overflow-hidden',
getColumnClassName?.(cell.column.id, 'cell')
)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{renderCellContent(cell)}
</TableCell>
))}
</TableRow>
Expand All @@ -61,3 +66,28 @@ export const DataTableRow = React.memo(DataTableRowInner, (prev, next) => {
prev.row.getIsSelected() === next.row.getIsSelected()
)
}) as typeof DataTableRowInner

function renderCellContent<TData>(cell: Cell<TData, unknown>) {
const content = flexRender(cell.column.columnDef.cell, cell.getContext())
const textContent = getPrimitiveTextContent(content)

if (!textContent) return content

return <TruncatedCell tooltipContent={textContent}>{content}</TruncatedCell>
}

function getPrimitiveTextContent(content: React.ReactNode): string | null {
if (typeof content === 'string' || typeof content === 'number') {
return String(content)
}

if (
React.isValidElement<{ children?: React.ReactNode }>(content) &&
(typeof content.props.children === 'string' ||
typeof content.props.children === 'number')
) {
return String(content.props.children)
}

return null
}
91 changes: 91 additions & 0 deletions web/default/src/components/data-table/core/truncated-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright (C) 2023-2026 QuantumNous

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'
import { cn } from '@/lib/utils'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'

type TruncatedCellProps = {
children: React.ReactNode
cellClassName?: string
className?: string
contentClassName?: string
side?: 'top' | 'bottom' | 'left' | 'right'
tooltipClassName?: string
tooltipContent?: React.ReactNode
}

export function TruncatedCell({
children,
cellClassName,
className,
contentClassName,
side = 'top',
tooltipClassName,
tooltipContent,
}: TruncatedCellProps) {
const content = tooltipContent ?? getTextContent(children)

if (!content) {
return (
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
>
{children}
</div>
)
}

return (
<Tooltip>
<TooltipTrigger
render={
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
/>
}
>
<div className={cn('truncate', contentClassName)}>{children}</div>
</TooltipTrigger>
<TooltipContent
side={side}
className={cn('max-w-xs break-all', tooltipClassName)}
>
{content}
</TooltipContent>
</Tooltip>
)
}

function getTextContent(node: React.ReactNode): string {
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(getTextContent).join('')
return ''
}
2 changes: 2 additions & 0 deletions web/default/src/components/data-table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/
export { DataTablePagination } from './core/pagination'
export { DataTableColumnHeader } from './core/column-header'
export { BadgeCell } from './core/badge-cell'
export { BadgeListCell } from './core/badge-list-cell'
export { TruncatedCell } from './core/truncated-cell'
export { DataTableViewOptions } from './toolbar/view-options'
export { DataTableToolbar } from './toolbar/toolbar'
export { DataTableBulkActions } from './toolbar/bulk-actions'
Expand Down
37 changes: 35 additions & 2 deletions web/default/src/components/data-table/static/static-data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { TruncatedCell } from '../core/truncated-cell'
import { staticDataTableClassNames } from './static-data-table-classnames'

type StaticDataTableBaseProps = {
Expand Down Expand Up @@ -163,15 +164,47 @@ function StaticDataTableRow<TData>({
{columns.map((column) => (
<TableCell
key={column.id}
className={getStaticCellClassName(column, row, index)}
className={cn(
'max-w-full min-w-0 overflow-hidden',
getStaticCellClassName(column, row, index)
)}
>
{column.cell?.(row, index)}
{renderStaticCellContent(column, row, index)}
</TableCell>
))}
</TableRow>
)
}

function renderStaticCellContent<TData>(
column: StaticDataTableColumn<TData>,
row: TData,
index: number
) {
const content = column.cell?.(row, index)
const textContent = getPrimitiveTextContent(content)

if (!textContent) return content

return <TruncatedCell tooltipContent={textContent}>{content}</TruncatedCell>
}

function getPrimitiveTextContent(content: React.ReactNode): string | null {
if (typeof content === 'string' || typeof content === 'number') {
return String(content)
}

if (
React.isValidElement<{ children?: React.ReactNode }>(content) &&
(typeof content.props.children === 'string' ||
typeof content.props.children === 'number')
) {
return String(content.props.children)
}

return null
}

function getStaticCellClassName<TData>(
column: StaticDataTableColumn<TData>,
row: TData,
Expand Down
8 changes: 5 additions & 3 deletions web/default/src/components/group-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function GroupBadge(props: GroupBadgeProps) {
ratio,
copyable = false,
showDot,
className,
...badgeProps
} = props
const groupName = group?.trim()
Expand All @@ -82,6 +83,7 @@ export function GroupBadge(props: GroupBadgeProps) {
showDot={showDot ?? (isSpecialGroup ? false : undefined)}
variant={isSpecialGroup ? 'neutral' : undefined}
autoColor={isSpecialGroup ? undefined : groupName}
className={cn('min-w-0 shrink overflow-hidden', className)}
/>
)

Expand All @@ -90,11 +92,11 @@ export function GroupBadge(props: GroupBadgeProps) {
}

return (
<span className='inline-flex items-center gap-2 text-xs'>
{badge}
<span className='inline-flex max-w-full min-w-0 items-center gap-2 text-xs'>
<span className='max-w-full min-w-0 overflow-hidden'>{badge}</span>
<span
className={cn(
'inline-flex h-5 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums',
'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums',
getGroupRatioClassName(ratio)
)}
>
Expand Down
18 changes: 13 additions & 5 deletions web/default/src/components/status-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { type LucideIcon } from 'lucide-react'
import { stringToColor } from '@/lib/colors'
import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'

export const dotColorMap = {
success: 'bg-success',
warning: 'bg-warning',
Expand Down Expand Up @@ -81,7 +82,8 @@ export type StatusBadgeType = 'badge' | 'text' | 'underline'

/** Context that lets ancestor components (e.g. MobileCardList field area)
* override the badge type without modifying every call site. */
export const StatusBadgeTypeContext = React.createContext<StatusBadgeType>('badge')
export const StatusBadgeTypeContext =
React.createContext<StatusBadgeType>('badge')

const sizeMap = {
sm: 'h-5 gap-1 px-1.5 text-xs leading-none',
Expand Down Expand Up @@ -153,23 +155,29 @@ export function StatusBadge({
) : null)

const isBadge = type === 'badge'
const title = copyable
? `Click to copy: ${copyText || label || ''}`
: label || undefined

return (
<span
data-slot='status-badge'
className={cn(
'inline-flex w-fit max-w-full shrink-0 items-center font-medium tracking-normal whitespace-nowrap transition-colors',
'inline-flex w-fit max-w-full min-w-0 shrink items-center font-medium tracking-normal whitespace-nowrap transition-colors',
isBadge
? cn('rounded-4xl', sizeMap[size ?? 'sm'])
: cn(textSizeMap[size ?? 'sm'], type === 'underline' && 'border-b border-current pb-px'),
: cn(
textSizeMap[size ?? 'sm'],
type === 'underline' && 'border-b border-current pb-px'
),
textColorMap[computedVariant],
pulse && 'animate-pulse',
copyable &&
'cursor-copy hover:brightness-95 active:scale-95 dark:hover:brightness-110',
className
)}
onClick={handleClick}
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
title={title}
{...props}
>
{showDot && (
Expand Down Expand Up @@ -221,7 +229,7 @@ export function StatusBadgeList<T>(props: StatusBadgeListProps<T>) {
return (
<div
className={cn(
'flex max-w-full items-center gap-1 overflow-hidden',
'flex max-w-full min-w-0 items-center gap-1 overflow-hidden',
className
)}
{...domProps}
Expand Down
Loading