merge: 同步上游 dev 最新内容
This commit is contained in:
@@ -76,7 +76,6 @@ function DynamicConfigSection({
|
||||
basePath,
|
||||
hooks,
|
||||
level,
|
||||
mergedChildren = [],
|
||||
nestedSchema,
|
||||
onChange,
|
||||
sectionDescription,
|
||||
@@ -87,11 +86,6 @@ function DynamicConfigSection({
|
||||
basePath: string
|
||||
hooks: FieldHookRegistry
|
||||
level: number
|
||||
mergedChildren?: Array<{
|
||||
key: string
|
||||
schema: ConfigSchema
|
||||
values: Record<string, unknown>
|
||||
}>
|
||||
nestedSchema: ConfigSchema
|
||||
onChange: (field: string, value: unknown) => void
|
||||
sectionDescription?: string
|
||||
@@ -100,9 +94,7 @@ function DynamicConfigSection({
|
||||
values: Record<string, unknown>
|
||||
}) {
|
||||
const [advancedVisible, setAdvancedVisible] = React.useState(false)
|
||||
const hasAdvanced =
|
||||
hasTopLevelAdvancedFields(nestedSchema) ||
|
||||
mergedChildren.some((child) => hasTopLevelAdvancedFields(child.schema))
|
||||
const hasAdvanced = hasTopLevelAdvancedFields(nestedSchema)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -135,37 +127,6 @@ function DynamicConfigSection({
|
||||
level={level}
|
||||
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
||||
/>
|
||||
{mergedChildren.map((child) => {
|
||||
const childTitle = resolveSectionTitle(child.schema)
|
||||
const childDescription = resolveSectionDescription(child.schema, childTitle)
|
||||
const parentPath = basePath.includes('.')
|
||||
? basePath.replace(/\.[^.]+$/, '')
|
||||
: ''
|
||||
const childPath = buildFieldPath(parentPath, child.key)
|
||||
|
||||
return (
|
||||
<div key={child.key} className="mt-5 border-t border-border/50 pt-4">
|
||||
<div className="mb-3 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<SectionIcon iconName={child.schema.uiIcon} />
|
||||
<h3 className="text-sm font-medium">{childTitle}</h3>
|
||||
</div>
|
||||
{childDescription && (
|
||||
<p className="text-xs text-muted-foreground">{childDescription}</p>
|
||||
)}
|
||||
</div>
|
||||
<DynamicConfigForm
|
||||
schema={child.schema}
|
||||
values={child.values}
|
||||
onChange={(field, value) => onChange(`${child.key}.${field}`, value)}
|
||||
basePath={childPath}
|
||||
hooks={hooks}
|
||||
level={level}
|
||||
advancedVisible={hasAdvanced ? advancedVisible : undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -197,17 +158,6 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
() => new Map(schema.fields.map((field) => [field.name, field])),
|
||||
[schema.fields],
|
||||
)
|
||||
const mergedChildKeys = React.useMemo(() => {
|
||||
const keys = new Set<string>()
|
||||
for (const nestedSchema of Object.values(schema.nested ?? {})) {
|
||||
for (const childKey of nestedSchema.uiMergeChildren ?? []) {
|
||||
if (schema.nested?.[childKey]) {
|
||||
keys.add(childKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}, [schema.nested])
|
||||
|
||||
const renderField = (field: FieldSchema) => {
|
||||
const fieldPath = buildFieldPath(basePath, field.name)
|
||||
@@ -225,6 +175,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
schema={field}
|
||||
parentValues={values}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -235,6 +186,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
schema={field}
|
||||
parentValues={values}
|
||||
>
|
||||
<DynamicField
|
||||
schema={field}
|
||||
@@ -265,12 +217,50 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
? [...normalFields, ...advancedFields]
|
||||
: normalFields
|
||||
|
||||
const groupFieldsByRow = (fields: FieldSchema[]) => {
|
||||
const rows: FieldSchema[][] = []
|
||||
let currentRow: FieldSchema[] = []
|
||||
let currentRowKey: string | undefined
|
||||
|
||||
for (const field of fields) {
|
||||
const rowKey = field['x-row']
|
||||
if (rowKey && rowKey === currentRowKey) {
|
||||
currentRow.push(field)
|
||||
continue
|
||||
}
|
||||
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow)
|
||||
}
|
||||
|
||||
currentRow = [field]
|
||||
currentRowKey = rowKey
|
||||
}
|
||||
|
||||
if (currentRow.length > 0) {
|
||||
rows.push(currentRow)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
const renderFieldList = (fields: FieldSchema[]) => (
|
||||
<>
|
||||
{fields.map((field, index) => (
|
||||
<React.Fragment key={field.name}>
|
||||
{groupFieldsByRow(fields).map((row, index) => (
|
||||
<React.Fragment key={row.map((field) => field.name).join('|')}>
|
||||
{index > 0 && <Separator className="my-2 bg-border/50" />}
|
||||
<div className="py-1">{renderField(field)}</div>
|
||||
{row.length > 1 ? (
|
||||
<div
|
||||
className="grid gap-4 py-1 md:grid-cols-[repeat(var(--field-row-count),minmax(0,1fr))]"
|
||||
style={{ '--field-row-count': row.length } as React.CSSProperties}
|
||||
>
|
||||
{row.map((field) => (
|
||||
<div key={field.name}>{renderField(field)}</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">{renderField(row[0])}</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
@@ -294,7 +284,6 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
|
||||
{schema.nested &&
|
||||
Object.entries(schema.nested)
|
||||
.filter(([key]) => !mergedChildKeys.has(key))
|
||||
.map(([key, nestedSchema]) => {
|
||||
const nestedField = fieldMap.get(key)
|
||||
const nestedFieldPath = buildFieldPath(basePath, key)
|
||||
@@ -313,6 +302,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
onChange={(v) => onChange(key, v)}
|
||||
schema={nestedField ?? nestedSchema}
|
||||
nestedSchema={nestedSchema}
|
||||
parentValues={values}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -326,6 +316,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
onChange={(v) => onChange(key, v)}
|
||||
schema={nestedField ?? nestedSchema}
|
||||
nestedSchema={nestedSchema}
|
||||
parentValues={values}
|
||||
>
|
||||
<DynamicConfigForm
|
||||
schema={nestedSchema}
|
||||
@@ -342,34 +333,11 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
|
||||
const sectionTitle = resolveSectionTitle(nestedSchema)
|
||||
const sectionDescription = resolveSectionDescription(nestedSchema, sectionTitle)
|
||||
const mergedChildren = (nestedSchema.uiMergeChildren ?? [])
|
||||
.map((childKey) => {
|
||||
const childSchema = schema.nested?.[childKey]
|
||||
if (!childSchema) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
key: childKey,
|
||||
schema: childSchema,
|
||||
values: (values[childKey] as Record<string, unknown>) || {},
|
||||
}
|
||||
})
|
||||
.filter(
|
||||
(
|
||||
child,
|
||||
): child is {
|
||||
key: string
|
||||
schema: ConfigSchema
|
||||
values: Record<string, unknown>
|
||||
} => Boolean(child),
|
||||
)
|
||||
|
||||
if (level === 0) {
|
||||
return (
|
||||
<DynamicConfigSection
|
||||
key={key}
|
||||
mergedChildren={mergedChildren}
|
||||
nestedSchema={nestedSchema}
|
||||
values={(values[key] as Record<string, unknown>) || {}}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -8,6 +8,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Slider } from "@/components/ui/slider"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import type { FieldSchema } from "@/types/config-schema"
|
||||
|
||||
@@ -31,6 +37,27 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const isNumericField = schema.type === 'integer' || schema.type === 'number'
|
||||
|
||||
const parseNumericValue = (rawValue: unknown, fallbackValue: unknown = 0) => {
|
||||
if (typeof rawValue === 'number' && Number.isFinite(rawValue)) {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
if (typeof rawValue === 'string') {
|
||||
const parsedValue = parseFloat(rawValue)
|
||||
if (Number.isFinite(parsedValue)) {
|
||||
return schema.type === 'integer' ? Math.trunc(parsedValue) : parsedValue
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackValue !== rawValue) {
|
||||
return parseNumericValue(fallbackValue, 0)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const renderPrimitiveArrayEditor = () => {
|
||||
const itemType = schema.items?.type ?? 'string'
|
||||
const arrayValue = Array.isArray(value)
|
||||
@@ -94,6 +121,10 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
return <IconComponent className="h-4 w-4" />
|
||||
}
|
||||
|
||||
const optionDescriptions = schema['x-option-descriptions'] ?? {}
|
||||
const hasOptionDescriptions = Object.keys(optionDescriptions).length > 0
|
||||
const inlineDescription = hasOptionDescriptions ? '' : schema.description
|
||||
|
||||
const renderFieldHeader = () => (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<Label
|
||||
@@ -108,9 +139,9 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
<span className="break-all">{schema.label}</span>
|
||||
{schema.required && <span className="text-destructive">*</span>}
|
||||
</Label>
|
||||
{schema.description && (
|
||||
{inlineDescription && (
|
||||
<span className="text-[13px] leading-6 text-muted-foreground whitespace-pre-line">
|
||||
{schema.description}
|
||||
{inlineDescription}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -122,10 +153,14 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
const renderInputComponent = () => {
|
||||
const widget = schema['x-widget']
|
||||
const type = schema.type
|
||||
const resolvedWidget =
|
||||
isNumericField && (widget === 'input' || widget === 'number' || !widget)
|
||||
? 'number'
|
||||
: widget
|
||||
|
||||
// x-widget 优先
|
||||
if (widget) {
|
||||
switch (widget) {
|
||||
if (resolvedWidget) {
|
||||
switch (resolvedWidget) {
|
||||
case 'slider':
|
||||
return renderSlider()
|
||||
case 'input':
|
||||
@@ -214,7 +249,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
* 渲染 Slider 组件(用于 number 类型 + x-widget: slider)
|
||||
*/
|
||||
const renderSlider = () => {
|
||||
const numValue = typeof value === 'number' ? value : (schema.default as number ?? 0)
|
||||
const numValue = parseNumericValue(value, schema.default)
|
||||
const min = schema.minValue ?? 0
|
||||
const max = schema.maxValue ?? 100
|
||||
const step = schema.step ?? 1
|
||||
@@ -241,7 +276,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
* 渲染 Input[type="number"] 组件(用于 number/integer 类型)
|
||||
*/
|
||||
const renderNumberInput = () => {
|
||||
const numValue = typeof value === 'number' ? value : (schema.default as number ?? 0)
|
||||
const numValue = parseNumericValue(value, schema.default)
|
||||
const min = schema.minValue
|
||||
const max = schema.maxValue
|
||||
const step = schema.step ?? (schema.type === 'integer' ? 1 : 0.1)
|
||||
@@ -250,7 +285,12 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
<Input
|
||||
type="number"
|
||||
value={numValue}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
||||
onChange={(e) => {
|
||||
const nextValue = schema.type === 'integer'
|
||||
? parseInt(e.target.value, 10)
|
||||
: parseFloat(e.target.value)
|
||||
onChange(Number.isFinite(nextValue) ? nextValue : 0)
|
||||
}}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
@@ -262,7 +302,12 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
* 渲染 Input[type="text"] 组件(用于 string 类型)
|
||||
*/
|
||||
const renderTextInput = (type: 'password' | 'text' = 'text') => {
|
||||
const strValue = typeof value === 'string' ? value : (schema.default as string ?? '')
|
||||
const strValue =
|
||||
typeof value === 'string'
|
||||
? value
|
||||
: value === null || value === undefined
|
||||
? String(schema.default ?? '')
|
||||
: String(value)
|
||||
return (
|
||||
<Input
|
||||
type={type}
|
||||
@@ -277,11 +322,19 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
*/
|
||||
const renderTextarea = () => {
|
||||
const strValue = typeof value === 'string' ? value : (schema.default as string ?? '')
|
||||
const minHeight = typeof schema['x-textarea-min-height'] === 'number'
|
||||
? schema['x-textarea-min-height']
|
||||
: undefined
|
||||
const rows = typeof schema['x-textarea-rows'] === 'number'
|
||||
? schema['x-textarea-rows']
|
||||
: 4
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
value={strValue}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={4}
|
||||
rows={rows}
|
||||
minHeight={minHeight}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -307,11 +360,39 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
|
||||
<SelectValue placeholder={`Select ${schema.label}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
{hasOptionDescriptions ? (
|
||||
<TooltipProvider delayDuration={150}>
|
||||
{options.map((option) => {
|
||||
const description = optionDescriptions[option]
|
||||
return description ? (
|
||||
<Tooltip key={option}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem value={option} title={description}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
className="max-w-72 bg-background text-foreground border shadow-lg"
|
||||
>
|
||||
{description}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
)
|
||||
})}
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
options.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
|
||||
@@ -100,6 +100,42 @@ describe('DynamicField', () => {
|
||||
|
||||
expect(screen.getByText('Custom field requires Hook')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders number Input when x-widget is input but type is integer', () => {
|
||||
const schema: FieldSchema = {
|
||||
name: 'test_integer_input_widget',
|
||||
type: 'integer',
|
||||
label: 'Test Integer Input Widget',
|
||||
description: 'A numeric field rendered as input',
|
||||
required: false,
|
||||
'x-widget': 'input',
|
||||
default: 0,
|
||||
}
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<DynamicField schema={schema} value={2} onChange={onChange} />)
|
||||
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue(2)
|
||||
})
|
||||
|
||||
it('parses string values for numeric input widgets', () => {
|
||||
const schema: FieldSchema = {
|
||||
name: 'test_string_number_input_widget',
|
||||
type: 'integer',
|
||||
label: 'Test String Number Input Widget',
|
||||
description: 'A numeric field with legacy string value',
|
||||
required: false,
|
||||
'x-widget': 'input',
|
||||
default: 0,
|
||||
}
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<DynamicField schema={schema} value="2" onChange={onChange} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toHaveValue(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('type fallback', () => {
|
||||
@@ -305,6 +341,27 @@ describe('DynamicField', () => {
|
||||
await user.type(input, '123')
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('triggers numeric onChange for input widget with integer type', async () => {
|
||||
const schema: FieldSchema = {
|
||||
name: 'test_integer_input_widget_change',
|
||||
type: 'integer',
|
||||
label: 'Test Integer Input Widget Change',
|
||||
description: 'A numeric field rendered as input',
|
||||
required: false,
|
||||
'x-widget': 'input',
|
||||
default: 0,
|
||||
}
|
||||
const onChange = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<DynamicField schema={schema} value={0} onChange={onChange} />)
|
||||
|
||||
const input = screen.getByRole('spinbutton')
|
||||
await user.clear(input)
|
||||
await user.type(input, '5')
|
||||
expect(onChange).toHaveBeenLastCalledWith(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('visual features', () => {
|
||||
@@ -377,6 +434,25 @@ describe('DynamicField', () => {
|
||||
expect(screen.getByText('50')).toBeInTheDocument()
|
||||
expect(screen.getByText('25')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('parses string values for slider widgets', () => {
|
||||
const schema: FieldSchema = {
|
||||
name: 'test_slider_string_value',
|
||||
type: 'number',
|
||||
label: 'Test Slider String Value',
|
||||
description: 'A slider with legacy string value',
|
||||
required: false,
|
||||
'x-widget': 'slider',
|
||||
minValue: 0,
|
||||
maxValue: 10,
|
||||
default: 0,
|
||||
}
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<DynamicField schema={schema} value="2.5" onChange={onChange} />)
|
||||
|
||||
expect(screen.getByText('2.5')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('select features', () => {
|
||||
|
||||
@@ -80,6 +80,7 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
// 快速审核模式状态
|
||||
const [quickFilterType, setQuickFilterType] = useState<'unchecked' | 'passed' | 'rejected' | 'all'>('unchecked')
|
||||
const [quickExpressions, setQuickExpressions] = useState<Expression[]>([])
|
||||
const quickExpressionsRef = useRef<Expression[]>([])
|
||||
const [quickCurrentIndex, setQuickCurrentIndex] = useState(0)
|
||||
const [quickLoading, setQuickLoading] = useState(false)
|
||||
const [quickTotal, setQuickTotal] = useState(0)
|
||||
@@ -92,6 +93,10 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const dragStartRef = useRef<{ x: number; y: number } | null>(null)
|
||||
const isDraggingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
quickExpressionsRef.current = quickExpressions
|
||||
}, [quickExpressions])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [statsLoading, setStatsLoading] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
@@ -180,9 +185,13 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
setQuickLoading(true)
|
||||
const pageToLoad = append ? quickPage + 1 : quickPage
|
||||
const result = await getReviewList({
|
||||
page: pageToLoad,
|
||||
page: quickFilterType === 'unchecked' ? 1 : pageToLoad,
|
||||
page_size: 20,
|
||||
filter_type: quickFilterType,
|
||||
order: quickFilterType === 'unchecked' ? 'random' : 'latest',
|
||||
exclude_ids: quickFilterType === 'unchecked' && append
|
||||
? quickExpressionsRef.current.map((expr) => expr.id)
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
@@ -552,8 +561,8 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
}
|
||||
|
||||
// 获取聊天名称
|
||||
const getChatName = (chatId: string): string => {
|
||||
return chatNameMap.get(chatId) || chatId
|
||||
const getChatName = (expression: Expression): string => {
|
||||
return expression.chat_name || chatNameMap.get(expression.chat_id) || expression.chat_id
|
||||
}
|
||||
|
||||
// 单条审核
|
||||
@@ -1104,8 +1113,8 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
|
||||
<div className="flex flex-wrap items-center gap-1 sm:gap-2 text-xs text-muted-foreground">
|
||||
<span>#{expr.id}</span>
|
||||
<span>·</span>
|
||||
<span title={getChatName(expr.chat_id)} className="truncate max-w-24 sm:max-w-32">
|
||||
{getChatName(expr.chat_id)}
|
||||
<span title={getChatName(expr)} className="truncate max-w-24 sm:max-w-32">
|
||||
{getChatName(expr)}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{formatTime(expr.create_date)}</span>
|
||||
@@ -1585,8 +1594,8 @@ if (isCurrent) {
|
||||
<div className="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||
<User className="h-3 w-3" />
|
||||
</div>
|
||||
<span title={getChatName(expr.chat_id)} className="truncate max-w-[120px] font-medium">
|
||||
{getChatName(expr.chat_id)}
|
||||
<span title={getChatName(expr)} className="truncate max-w-[120px] font-medium">
|
||||
{getChatName(expr)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono">{formatTime(expr.create_date)}</span>
|
||||
@@ -1638,8 +1647,8 @@ if (isCurrent) {
|
||||
<div className="w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
||||
<User className="h-3 w-3" />
|
||||
</div>
|
||||
<span title={getChatName(expr.chat_id)} className="truncate max-w-[120px] font-medium">
|
||||
{getChatName(expr.chat_id)}
|
||||
<span title={getChatName(expr)} className="truncate max-w-[120px] font-medium">
|
||||
{getChatName(expr)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono">{formatTime(expr.create_date)}</span>
|
||||
|
||||
Reference in New Issue
Block a user