fix(web/channel): stabilize inline priority updates (#6415)

* fix(channel): debounce inline priority updates

* fix(channel): commit spinner edits only on Enter or focus leave

Blurring the inline edit input to the +/- buttons flushed the pending
priority update immediately, bypassing the container focus-containment
check and defeating the debounce. Commit now happens via the container
blur handler or explicitly on Enter. Add unit tests for the priority
update scheduler.

* fix(channel): preserve row identity when priority updates reorder channels
This commit is contained in:
RedwindA
2026-07-25 15:59:59 +08:00
committed by GitHub
parent bf8cfcc512
commit a0d0e5049e
8 changed files with 327 additions and 41 deletions
@@ -27,7 +27,7 @@ import {
Shuffle,
SlidersHorizontal,
} from 'lucide-react'
import { useState, useMemo, useContext } from 'react'
import { useState, useMemo, useContext, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -71,6 +71,7 @@ import {
handleUpdateChannelField,
handleUpdateTagField,
handleUpdateChannelBalance,
createChannelPriorityUpdateScheduler,
isTagAggregateRow,
type TagRow,
} from '../lib'
@@ -172,55 +173,77 @@ function UpstreamUpdateTags({ channel }: { channel: Channel }) {
* Priority cell component with inline editing
*/
function PriorityCell({ channel }: { channel: Channel }) {
if (isTagAggregateRow(channel)) {
return <TagPriorityCell channel={channel} />
}
return <ChannelPriorityCell channel={channel} />
}
function TagPriorityCell({ channel }: { channel: TagRow }) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const isTagRow = isTagAggregateRow(channel)
const priority = channel.priority
const [confirmOpen, setConfirmOpen] = useState(false)
const [pendingValue, setPendingValue] = useState<number | null>(null)
const tag = channel.tag || ''
const channelCount = channel.children?.length || 0
// Tag row - editable with confirmation for all tag channels
if (isTagRow) {
const tag = channel.tag || ''
const channelCount = channel.children?.length || 0
return (
<>
<NumericSpinnerInput
value={priority ?? 0}
onChange={(value) => {
setPendingValue(value)
setConfirmOpen(true)
}}
min={-999}
/>
<ConfirmDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
title={t('Confirm Batch 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)
}
setConfirmOpen(false)
}}
/>
</>
)
}
return (
<>
<NumericSpinnerInput
value={priority ?? 0}
onChange={(value) => {
setPendingValue(value)
setConfirmOpen(true)
}}
min={-999}
/>
<ConfirmDialog
open={confirmOpen}
onOpenChange={setConfirmOpen}
title={t('Confirm Batch 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)
}
setConfirmOpen(false)
}}
/>
</>
)
}
function ChannelPriorityCell({ channel }: { channel: Channel }) {
const queryClient = useQueryClient()
const priorityUpdateScheduler = useMemo(
() =>
createChannelPriorityUpdateScheduler((value) => {
void handleUpdateChannelField(
channel.id,
'priority',
value,
queryClient
)
}),
[channel.id, queryClient]
)
useEffect(
() => () => priorityUpdateScheduler.flush(),
[priorityUpdateScheduler]
)
// Regular channel row - editable
return (
<NumericSpinnerInput
value={priority ?? 0}
onChange={(value) => {
handleUpdateChannelField(channel.id, 'priority', value, queryClient)
}}
value={channel.priority ?? 0}
onChange={priorityUpdateScheduler.schedule}
onCommit={priorityUpdateScheduler.flush}
min={-999}
/>
)