Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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。
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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ export function ChannelsPrimaryButtons() {
open={showDeleteDialog}
onOpenChange={setShowDeleteDialog}
title={t('Delete All Disabled Channels?')}
desc='This will permanently delete all manually and automatically disabled channels. This action cannot be undone.'
desc={t(
'This will permanently delete all manually and automatically disabled channels. This action cannot be undone.'
)}
destructive
handleConfirm={() => {
handleDeleteAllDisabled(queryClient, (_count) => {
Expand Down
Loading