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 ''
Comment on lines +87 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle React element children in text extraction.

On Line 87, getTextContent skips valid React elements, so <TruncatedCell><span>foo</span></TruncatedCell> won’t derive tooltip text and silently drops the tooltip fallback path.

Suggested fix
 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('')
+  if (React.isValidElement(node)) return getTextContent(node.props.children)
   return ''
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/components/data-table/core/truncated-cell.tsx` around lines
87 - 90, The getTextContent function currently handles strings, numbers, and
arrays but returns an empty string for React elements, causing tooltips to be
skipped for components like span or other React elements. Extend the function to
check if the node is a React element (using React.isValidElement or similar
check), and if so, recursively extract text from its children property or
children prop to preserve the text content from nested React elements rather
than silently dropping it with an empty 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
Comment on lines +158 to +160

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the new title text.

On Line 159, "Click to copy: ..." is hardcoded English and bypasses i18n.

Suggested fix
 import * as React from 'react'
+import { t } from 'i18next'
 ...
   const title = copyable
-    ? `Click to copy: ${copyText || label || ''}`
+    ? t('Click to copy: {{value}}', { value: copyText || label || '' })
     : label || undefined

As per coding guidelines, “All user-facing text content must support i18n using the t() function from useTranslation() in React components.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/components/status-badge.tsx` around lines 158 - 160, The
hardcoded English string "Click to copy: " in the title variable assignment
bypasses i18n and violates localization guidelines. Extract the "Click to copy:
" text and wrap it with the t() function from useTranslation() to properly
localize this user-facing message. Ensure the component is using the
useTranslation hook and replace the hardcoded string with a translation key that
includes both the instructional text and the dynamic content (copyText or
label).

Source: Coding guidelines


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