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
2 changes: 1 addition & 1 deletion web/default/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
],
"import/first": "warn",
"import/newline-after-import": "warn",
"import/no-cycle": "warn",
"import/no-cycle": "error",
"import/no-duplicates": [
"error",
{
Expand Down
2 changes: 2 additions & 0 deletions web/default/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
- **可读性**:控制函数圈复杂度,复杂逻辑拆成小函数;变量与函数命名需有意义,遵循驼峰等常规约定。
- **TypeScript**:避免 `any`,优先具体类型或 `unknown`;为参数与返回值显式标注类型;仅类型用途的导入使用 `import type { X } from '...'`。
- **类型检查**:每次改动 TypeScript 或 TSX 代码后都要执行类型检查(如 `bun run typecheck`);若出现类型错误,须修复至无错误为止,不得遗留。
- **Lint 检查**:每次完成代码改动前,必须对所涉及文件执行 lint 检查,并修复这些文件中的所有 lint error;不得遗留 error。warning 可按变更范围与风险评估处理。
- **解构**:对象非必要不要进行解构,特别是组件的 props;直接使用 `props.xxx` 更清晰,避免不必要的解构增加代码复杂度。

### 3.3 组件
Expand Down Expand Up @@ -176,3 +177,4 @@
- **2026-01-28**:补充状态管理、API、表单、路由、错误处理、样式、文件组织、可访问性、安全、测试、依赖与构建部署规范。
- **2026-01-29**:重组文档结构,合并重复内容,明确主次与交叉引用。
- **2026-01-31**:在 3.2 中补充「类型检查」要求:改动 TS/TSX 后须执行 typecheck 并修复至无错。
- **2026-06-21**:在 3.2 中补充「Lint 检查」要求:完成代码改动前须修复所涉及文件的所有 lint error。
1 change: 1 addition & 0 deletions web/default/src/components/data-table/core/badge-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type BadgeCellProps = React.HTMLAttributes<HTMLDivElement>
export function BadgeCell({ className, ...props }: BadgeCellProps) {
return (
<div
data-slot='badge-cell'
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
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
*/
export function isContentSizedColumn(columnId: string): boolean {
return columnId === 'actions'
}
33 changes: 21 additions & 12 deletions web/default/src/components/data-table/core/data-table-colgroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,36 @@ For commercial licensing, please contact support@quantumnous.com
*/
import type { Table as TanstackTable } from '@tanstack/react-table'

import { isContentSizedColumn } from './content-sized-columns'

export function DataTableColgroup<TData>({
table,
}: {
table: TanstackTable<TData>
}) {
const columns = table.getVisibleLeafColumns()
const totalSize = columns.reduce((sum, col) => sum + col.getSize(), 0)
const sizedColumns = columns.filter(
(column) => !isContentSizedColumn(column.id)
)
const totalSize = sizedColumns.reduce((sum, col) => sum + col.getSize(), 0)

return (
<colgroup>
{columns.map((column) => (
<col
key={column.id}
style={{
width:
totalSize > 0
? `${(column.getSize() / totalSize) * 100}%`
: undefined,
}}
/>
))}
{columns.map((column) => {
const width = isContentSizedColumn(column.id)
? undefined
: getColumnWidth(column.getSize(), totalSize)

return <col key={column.id} style={{ width }} />
})}
</colgroup>
)
}

function getColumnWidth(columnSize: number, totalSize: number) {
if (totalSize <= 0) {
return undefined
}

return `${(columnSize / totalSize) * 100}%`
}
14 changes: 13 additions & 1 deletion web/default/src/components/data-table/core/data-table-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@tanstack/react-table'
import { TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { DataTableColumnHeader } from './column-header'
import { isContentSizedColumn } from './content-sized-columns'
import type { DataTableColumnClassName } from './types'

type DataTableHeaderProps<TData> = {
Expand All @@ -49,7 +50,7 @@ export function DataTableHeader<TData>({
key={header.id}
colSpan={header.colSpan}
className={getColumnClassName?.(header.column.id, 'header')}
style={applyHeaderSize ? { width: header.getSize() } : undefined}
style={getHeaderSizeStyle(header, applyHeaderSize)}
>
{renderHeaderContent(header)}
</TableHead>
Expand All @@ -60,6 +61,17 @@ export function DataTableHeader<TData>({
)
}

function getHeaderSizeStyle<TData>(
header: Header<TData, unknown>,
applyHeaderSize: boolean | undefined
) {
if (!applyHeaderSize || isContentSizedColumn(header.column.id)) {
return undefined
}

return { width: header.getSize() }
}

function renderHeaderContent<TData>(header: Header<TData, unknown>) {
if (header.isPlaceholder) return null
const { header: headerDef, meta } = header.column.columnDef
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Row, Table as TanstackTable } from '@tanstack/react-table'
/*
Copyright (C) 2023-2026 QuantumNous

Expand All @@ -18,6 +17,7 @@ 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 type { Row, Table as TanstackTable } from '@tanstack/react-table'

import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table'
import { cn } from '@/lib/utils'
Expand Down Expand Up @@ -45,6 +45,7 @@ export type {
DataTableViewProps,
} from './types'
export { DataTableRow } from './data-table-row'
export { DataTableRowActionMenu } from './row-action-menu'

export function DataTableView<TData>(props: DataTableViewProps<TData>) {
const rows = props.rows ?? props.table.getRowModel().rows
Expand Down
60 changes: 60 additions & 0 deletions web/default/src/components/data-table/core/row-action-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
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 { MoreHorizontal } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'

type DataTableRowActionMenuProps = {
children: React.ReactNode
ariaLabel: string
contentClassName?: string
modal?: boolean
onOpenChange?: (open: boolean) => void
}

export function DataTableRowActionMenu(props: DataTableRowActionMenuProps) {
return (
<DropdownMenu modal={props.modal} onOpenChange={props.onOpenChange}>
<DropdownMenuTrigger
render={
<Button
variant='ghost'
size='icon'
className='data-popup-open:bg-muted'
aria-label={props.ariaLabel}
/>
}
>
<MoreHorizontal aria-hidden='true' />
</DropdownMenuTrigger>
<DropdownMenuContent
align='end'
className={cn('w-48', props.contentClassName)}
>
{props.children}
</DropdownMenuContent>
</DropdownMenu>
)
}
3 changes: 3 additions & 0 deletions web/default/src/components/data-table/core/table-sizing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import type * as React from 'react'
import type { Table as TanstackTable } from '@tanstack/react-table'

import { isContentSizedColumn } from './content-sized-columns'

export function getTableSizeStyle<TData>(
table: TanstackTable<TData>
): React.CSSProperties {
const width = table
.getVisibleLeafColumns()
.filter((column) => !isContentSizedColumn(column.id))
.reduce((total, column) => total + column.getSize(), 0)

return {
Expand Down
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 @@ -28,9 +28,11 @@ export {
StaticDataTable,
type StaticDataTableColumn,
} from './static/static-data-table'
export { StaticRowActions } from './static/static-row-actions'
export { staticDataTableClassNames } from './static/static-data-table-classnames'
export {
DataTableRow,
DataTableRowActionMenu,
DataTableView,
type DataTableColumnClassName,
type DataTablePinnedColumn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function CompactContent<TData>({ row }: { row: Row<TData> }) {
{label}
</div>
)}
<div className='min-w-0 overflow-hidden text-xs [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'>
<div className='min-w-0 overflow-hidden text-xs [&_:is([data-slot=badge-cell],[data-slot=provider-badge],[data-slot=status-badge])]:ml-0'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell) ?? '-'}
</StatusBadgeTypeContext.Provider>
Expand Down Expand Up @@ -146,7 +146,7 @@ function FallbackContent<TData>({ row }: { row: Row<TData> }) {
return (
<div
key={cell.id}
className='flex justify-end overflow-hidden [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'
className='flex justify-end overflow-hidden [&_:is([data-slot=badge-cell],[data-slot=provider-badge],[data-slot=status-badge])]:ml-0'
>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell)}
Expand All @@ -163,7 +163,7 @@ function FallbackContent<TData>({ row }: { row: Row<TData> }) {
<span className='text-muted-foreground shrink-0 text-[10px] font-medium select-none'>
{label}
</span>
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs [&_[data-slot=provider-badge]]:ml-0 [&_[data-slot=status-badge]]:ml-0'>
<div className='flex min-w-0 flex-1 items-center justify-end overflow-hidden text-xs [&_:is([data-slot=badge-cell],[data-slot=provider-badge],[data-slot=status-badge])]:ml-0'>
<StatusBadgeTypeContext.Provider value='text'>
{renderCellContent(cell) ?? '-'}
</StatusBadgeTypeContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import * as React from 'react'

import { PageFooterPortal } from '@/components/layout'
import { PageFooterPortal } from '@/components/layout/components/page-footer'
import { useMediaQuery } from '@/hooks'
import { cn } from '@/lib/utils'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ export const staticDataTableClassNames = {
mutedCodeCell: 'text-muted-foreground font-mono text-sm',
topNumericCell: 'py-2 text-right font-mono',
mediumCell: 'font-medium',
actionHeaderCell: 'text-right',
actionCell: 'text-right',
actionHeaderCell: 'w-auto max-w-none text-right',
actionCell: 'w-auto max-w-none text-right',
} as const
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
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 { Pencil, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenuItem,
DropdownMenuShortcut,
} from '@/components/ui/dropdown-menu'
import { DataTableRowActionMenu } from '../core/row-action-menu'

type StaticRowActionsProps = {
editLabel: string
deleteLabel: string
menuLabel: string
onEdit: () => void
onDelete: () => void
editDisabled?: boolean
deleteDisabled?: boolean
}

export function StaticRowActions(props: StaticRowActionsProps) {
return (
<div className='flex justify-end gap-1'>
<Button
variant='ghost'
size='icon-sm'
onClick={props.onEdit}
disabled={props.editDisabled}
aria-label={props.editLabel}
>
<Pencil />
</Button>
<DataTableRowActionMenu ariaLabel={props.menuLabel}>
<DropdownMenuItem
onClick={props.onDelete}
disabled={props.deleteDisabled}
className='text-destructive focus:text-destructive'
>
{props.deleteLabel}
<DropdownMenuShortcut>
<Trash2 size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DataTableRowActionMenu>
</div>
)
}
2 changes: 1 addition & 1 deletion web/default/src/components/truncated-text.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cn } from '@/lib/utils'
import { TruncatedCell } from '@/components/data-table'
import { TruncatedCell } from '@/components/data-table/core/truncated-cell'

interface TruncatedTextProps {
text: string
Expand Down
15 changes: 10 additions & 5 deletions web/default/src/features/channels/components/channels-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,11 @@ function PriorityCell({ channel }: { channel: Channel }) {
open={confirmOpen}
onOpenChange={setConfirmOpen}
title={t('Confirm Batch Update')}
desc={`This will update the priority to ${pendingValue} for all ${channelCount} channel(s) with tag "${tag}". Continue?`}
confirmText='Update'
desc={t(
'This will update the priority to {{value}} for all {{count}} channel(s) with tag "{{tag}}". Continue?',
{ value: pendingValue, count: channelCount, tag }
)}
confirmText={t('Update')}
handleConfirm={() => {
if (pendingValue !== null) {
handleUpdateTagField(tag, 'priority', pendingValue, queryClient)
Expand Down Expand Up @@ -255,8 +258,11 @@ function WeightCell({ channel }: { channel: Channel }) {
open={confirmOpen}
onOpenChange={setConfirmOpen}
title={t('Confirm Batch Update')}
desc={`This will update the weight to ${pendingValue} for all ${channelCount} channel(s) with tag "${tag}". Continue?`}
confirmText='Update'
desc={t(
'This will update the weight to {{value}} for all {{count}} channel(s) with tag "{{tag}}". Continue?',
{ value: pendingValue, count: channelCount, tag }
)}
confirmText={t('Update')}
handleConfirm={() => {
if (pendingValue !== null) {
handleUpdateTagField(tag, 'weight', pendingValue, queryClient)
Expand Down Expand Up @@ -1108,7 +1114,6 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {

return <DataTableRowActions row={row} />
},
size: 132,
enableSorting: false,
enableHiding: false,
meta: { pinned: 'right' as const },
Expand Down
Loading