fix: address small CodeRabbit issues (#9229)

## Summary

Address several small CodeRabbit-filed issues: clipboard simplification,
queue getter cleanup, pointer handling, and test parameterization.

## Changes

- **What**:
- Simplify `useCopyToClipboard` by using VueUse's built-in `legacy` mode
instead of a manual `document.execCommand` fallback
- Remove `queueIndex` getter alias from `TaskItemImpl`, replace all
usages with `job.priority`
- Add `pointercancel` event handling and try-catch around
`releasePointerCapture` in `useNodeResize` to prevent stuck resize state
- Parameterize repetitive `getNodeProvider` tests in
`modelToNodeStore.test.ts` using `it.each()`

- Fixes #9024
- Fixes #7955
- Fixes #7323
- Fixes #8703

## Review Focus

- `useCopyToClipboard`: VueUse's `legacy: true` enables the
`execCommand` fallback internally — verify browser compat is acceptable
- `useNodeResize`: cleanup logic extracted into shared function used by
both `pointerup` and `pointercancel`
This commit is contained in:
Johnpaul Chiwetelu
2026-02-26 11:32:53 +01:00
committed by GitHub
parent aef299caf8
commit 45ca1beea2
9 changed files with 122 additions and 197 deletions

View File

@@ -131,11 +131,11 @@ export const Queued: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-1'
const queueIndex = 104
const priority = 104
// Current job in pending
queue.pendingTasks = [
makePendingTask(jobId, queueIndex, Date.now() - 90_000)
makePendingTask(jobId, priority, Date.now() - 90_000)
]
// Add some other pending jobs to give context
queue.pendingTasks.push(
@@ -179,13 +179,13 @@ export const QueuedParallel: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-parallel'
const queueIndex = 210
const priority = 210
// Current job in pending with some ahead
queue.pendingTasks = [
makePendingTask('job-ahead-1', 200, Date.now() - 180_000),
makePendingTask('job-ahead-2', 205, Date.now() - 150_000),
makePendingTask(jobId, queueIndex, Date.now() - 120_000)
makePendingTask(jobId, priority, Date.now() - 120_000)
]
// Seen 2 minutes ago - set via prompt metadata above
@@ -238,9 +238,9 @@ export const Running: Story = {
const exec = useExecutionStore()
const jobId = 'job-running-1'
const queueIndex = 300
const priority = 300
queue.runningTasks = [
makeRunningTask(jobId, queueIndex, Date.now() - 65_000)
makeRunningTask(jobId, priority, Date.now() - 65_000)
]
queue.historyTasks = [
makeHistoryTask('hist-r1', 250, 30, true),
@@ -279,10 +279,10 @@ export const QueuedZeroAheadSingleRunning: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-zero-ahead-single'
const queueIndex = 510
const priority = 510
queue.pendingTasks = [
makePendingTask(jobId, queueIndex, Date.now() - 45_000)
makePendingTask(jobId, priority, Date.now() - 45_000)
]
queue.historyTasks = [
@@ -324,10 +324,10 @@ export const QueuedZeroAheadMultiRunning: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-zero-ahead-multi'
const queueIndex = 520
const priority = 520
queue.pendingTasks = [
makePendingTask(jobId, queueIndex, Date.now() - 20_000)
makePendingTask(jobId, priority, Date.now() - 20_000)
]
queue.historyTasks = [
@@ -380,8 +380,8 @@ export const Completed: Story = {
const queue = useQueueStore()
const jobId = 'job-completed-1'
const queueIndex = 400
queue.historyTasks = [makeHistoryTask(jobId, queueIndex, 37, true)]
const priority = 400
queue.historyTasks = [makeHistoryTask(jobId, priority, 37, true)]
return { args: { ...args, jobId } }
},
@@ -401,11 +401,11 @@ export const Failed: Story = {
const queue = useQueueStore()
const jobId = 'job-failed-1'
const queueIndex = 410
const priority = 410
queue.historyTasks = [
makeHistoryTask(
jobId,
queueIndex,
priority,
12,
false,
'Example error: invalid inputs for node X'

View File

@@ -168,14 +168,14 @@ const queuedAtValue = computed(() =>
const currentQueueIndex = computed<number | null>(() => {
const task = taskForJob.value
return task ? Number(task.queueIndex) : null
return task ? Number(task.job.priority) : null
})
const jobsAhead = computed<number | null>(() => {
const idx = currentQueueIndex.value
if (idx == null) return null
const ahead = queueStore.pendingTasks.filter(
(t: TaskItemImpl) => Number(t.queueIndex) < idx
(t: TaskItemImpl) => Number(t.job.priority) < idx
)
return ahead.length
})

View File

@@ -11,7 +11,7 @@ import type { TaskItemImpl } from '@/stores/queueStore'
type TestTask = {
jobId: string
queueIndex: number
job: { priority: number }
mockState: JobState
executionTime?: number
executionEndTimestamp?: number
@@ -174,7 +174,7 @@ const createTask = (
overrides: Partial<TestTask> & { mockState?: JobState } = {}
): TestTask => ({
jobId: overrides.jobId ?? `task-${Math.random().toString(36).slice(2, 7)}`,
queueIndex: overrides.queueIndex ?? 0,
job: overrides.job ?? { priority: 0 },
mockState: overrides.mockState ?? 'pending',
executionTime: overrides.executionTime,
executionEndTimestamp: overrides.executionEndTimestamp,
@@ -258,7 +258,7 @@ describe('useJobList', () => {
it('tracks recently added pending jobs and clears the hint after expiry', async () => {
vi.useFakeTimers()
queueStoreMock.pendingTasks = [
createTask({ jobId: '1', queueIndex: 1, mockState: 'pending' })
createTask({ jobId: '1', job: { priority: 1 }, mockState: 'pending' })
]
const { jobItems } = initComposable()
@@ -287,7 +287,7 @@ describe('useJobList', () => {
vi.useFakeTimers()
const taskId = '2'
queueStoreMock.pendingTasks = [
createTask({ jobId: taskId, queueIndex: 1, mockState: 'pending' })
createTask({ jobId: taskId, job: { priority: 1 }, mockState: 'pending' })
]
const { jobItems } = initComposable()
@@ -300,7 +300,7 @@ describe('useJobList', () => {
vi.mocked(buildJobDisplay).mockClear()
queueStoreMock.pendingTasks = [
createTask({ jobId: taskId, queueIndex: 2, mockState: 'pending' })
createTask({ jobId: taskId, job: { priority: 2 }, mockState: 'pending' })
]
await flush()
jobItems.value
@@ -314,7 +314,7 @@ describe('useJobList', () => {
it('cleans up timeouts on unmount', async () => {
vi.useFakeTimers()
queueStoreMock.pendingTasks = [
createTask({ jobId: '3', queueIndex: 1, mockState: 'pending' })
createTask({ jobId: '3', job: { priority: 1 }, mockState: 'pending' })
]
initComposable()
@@ -331,7 +331,7 @@ describe('useJobList', () => {
queueStoreMock.pendingTasks = [
createTask({
jobId: 'p',
queueIndex: 1,
job: { priority: 1 },
mockState: 'pending',
createTime: 3000
})
@@ -339,7 +339,7 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'r',
queueIndex: 5,
job: { priority: 5 },
mockState: 'running',
createTime: 2000
})
@@ -347,7 +347,7 @@ describe('useJobList', () => {
queueStoreMock.historyTasks = [
createTask({
jobId: 'h',
queueIndex: 3,
job: { priority: 3 },
mockState: 'completed',
createTime: 1000,
executionEndTimestamp: 5000
@@ -366,9 +366,9 @@ describe('useJobList', () => {
it('filters by job tab and resets failed tab when failures disappear', async () => {
queueStoreMock.historyTasks = [
createTask({ jobId: 'c', queueIndex: 3, mockState: 'completed' }),
createTask({ jobId: 'f', queueIndex: 2, mockState: 'failed' }),
createTask({ jobId: 'p', queueIndex: 1, mockState: 'pending' })
createTask({ jobId: 'c', job: { priority: 3 }, mockState: 'completed' }),
createTask({ jobId: 'f', job: { priority: 2 }, mockState: 'failed' }),
createTask({ jobId: 'p', job: { priority: 1 }, mockState: 'pending' })
]
const instance = initComposable()
@@ -384,7 +384,7 @@ describe('useJobList', () => {
expect(instance.hasFailedJobs.value).toBe(true)
queueStoreMock.historyTasks = [
createTask({ jobId: 'c', queueIndex: 3, mockState: 'completed' })
createTask({ jobId: 'c', job: { priority: 3 }, mockState: 'completed' })
]
await flush()
@@ -396,13 +396,13 @@ describe('useJobList', () => {
queueStoreMock.pendingTasks = [
createTask({
jobId: 'wf-1',
queueIndex: 2,
job: { priority: 2 },
mockState: 'pending',
workflowId: 'workflow-1'
}),
createTask({
jobId: 'wf-2',
queueIndex: 1,
job: { priority: 1 },
mockState: 'pending',
workflowId: 'workflow-2'
})
@@ -426,14 +426,14 @@ describe('useJobList', () => {
queueStoreMock.historyTasks = [
createTask({
jobId: 'alpha',
queueIndex: 2,
job: { priority: 2 },
mockState: 'completed',
createTime: 2000,
executionEndTimestamp: 2000
}),
createTask({
jobId: 'beta',
queueIndex: 1,
job: { priority: 1 },
mockState: 'failed',
createTime: 1000,
executionEndTimestamp: 1000
@@ -471,13 +471,13 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'active',
queueIndex: 3,
job: { priority: 3 },
mockState: 'running',
executionTime: 7_200_000
}),
createTask({
jobId: 'other',
queueIndex: 2,
job: { priority: 2 },
mockState: 'running',
executionTime: 3_600_000
})
@@ -507,7 +507,7 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'live-preview',
queueIndex: 1,
job: { priority: 1 },
mockState: 'running'
})
]
@@ -526,7 +526,7 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'disabled-preview',
queueIndex: 1,
job: { priority: 1 },
mockState: 'running'
})
]
@@ -567,28 +567,28 @@ describe('useJobList', () => {
queueStoreMock.historyTasks = [
createTask({
jobId: 'today-small',
queueIndex: 4,
job: { priority: 4 },
mockState: 'completed',
executionEndTimestamp: Date.now(),
executionTime: 2_000
}),
createTask({
jobId: 'today-large',
queueIndex: 3,
job: { priority: 3 },
mockState: 'completed',
executionEndTimestamp: Date.now(),
executionTime: 5_000
}),
createTask({
jobId: 'yesterday',
queueIndex: 2,
job: { priority: 2 },
mockState: 'failed',
executionEndTimestamp: Date.now() - 86_400_000,
executionTime: 1_000
}),
createTask({
jobId: 'undated',
queueIndex: 1,
job: { priority: 1 },
mockState: 'pending'
})
]

View File

@@ -4,64 +4,32 @@ import { useToast } from 'primevue/usetoast'
import { t } from '@/i18n'
export function useCopyToClipboard() {
const { copy, copied } = useClipboard()
const { copy, copied } = useClipboard({ legacy: true })
const toast = useToast()
const showSuccessToast = () => {
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('clipboard.successMessage'),
life: 3000
})
}
const showErrorToast = () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('clipboard.errorMessage')
})
}
function fallbackCopy(text: string) {
const textarea = document.createElement('textarea')
textarea.setAttribute('readonly', '')
textarea.value = text
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
textarea.setAttribute('aria-hidden', 'true')
textarea.setAttribute('tabindex', '-1')
textarea.style.width = '1px'
textarea.style.height = '1px'
document.body.appendChild(textarea)
textarea.select()
try {
// using legacy document.execCommand for fallback for old and linux browsers
const successful = document.execCommand('copy')
if (successful) {
showSuccessToast()
} else {
showErrorToast()
}
} catch (err) {
showErrorToast()
} finally {
textarea.remove()
}
}
const copyToClipboard = async (text: string) => {
async function copyToClipboard(text: string) {
try {
await copy(text)
if (copied.value) {
showSuccessToast()
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('clipboard.successMessage'),
life: 3000
})
} else {
// If VueUse copy failed, try fallback
fallbackCopy(text)
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('clipboard.errorMessage')
})
}
} catch (err) {
// VueUse copy failed, try fallback
fallbackCopy(text)
} catch {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('clipboard.errorMessage')
})
}
}

View File

@@ -189,25 +189,36 @@ export function useNodeResize(
}
}
const cleanup = () => {
if (!isResizing.value) return
isResizing.value = false
layoutStore.isResizingVueNodes.value = false
resizeStartPointer.value = null
resizeStartSize.value = null
resizeStartPosition.value = null
// Stop tracking shift key state
stopShiftSync()
stopMoveListen()
stopUpListen()
stopCancelListen()
}
const handlePointerUp = (upEvent: PointerEvent) => {
if (isResizing.value) {
isResizing.value = false
layoutStore.isResizingVueNodes.value = false
resizeStartPointer.value = null
resizeStartSize.value = null
resizeStartPosition.value = null
// Stop tracking shift key state
stopShiftSync()
target.releasePointerCapture(upEvent.pointerId)
stopMoveListen()
stopUpListen()
try {
target.releasePointerCapture(upEvent.pointerId)
} catch {
// Pointer capture may already be released
}
cleanup()
}
}
const stopMoveListen = useEventListener('pointermove', handlePointerMove)
const stopUpListen = useEventListener('pointerup', handlePointerUp)
const stopCancelListen = useEventListener('pointercancel', cleanup)
}
return {

View File

@@ -190,78 +190,28 @@ describe('useModelToNodeStore', () => {
expect(provider?.key).toBe('')
})
it('should return provider for new extension model types', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
it.each([
['sam2', 'DownloadAndLoadSAM2Model', 'model'],
['sams', 'SAMLoader', 'model_name'],
['ipadapter', 'IPAdapterModelLoader', 'ipadapter_file'],
['depthanything', 'DownloadAndLoadDepthAnythingV2Model', 'model'],
['ultralytics/bbox', 'UltralyticsDetectorProvider', 'model_name'],
['ultralytics/segm', 'UltralyticsDetectorProvider', 'model_name'],
['FlashVSR', 'FlashVSRNode', ''],
['FlashVSR-v1.1', 'FlashVSRNode', ''],
['segformer_b2_clothes', 'LS_LoadSegformerModel', 'model_name'],
['segformer_b3_fashion', 'LS_LoadSegformerModel', 'model_name']
])(
'should return correct provider for %s',
(modelType, expectedNodeName, expectedKey) => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
// SAM2
const sam2Provider = modelToNodeStore.getNodeProvider('sam2')
expect(sam2Provider?.nodeDef?.name).toBe('DownloadAndLoadSAM2Model')
expect(sam2Provider?.key).toBe('model')
// SAMLoader (original SAM)
const samsProvider = modelToNodeStore.getNodeProvider('sams')
expect(samsProvider?.nodeDef?.name).toBe('SAMLoader')
expect(samsProvider?.key).toBe('model_name')
// IP-Adapter
const ipadapterProvider = modelToNodeStore.getNodeProvider('ipadapter')
expect(ipadapterProvider?.nodeDef?.name).toBe('IPAdapterModelLoader')
expect(ipadapterProvider?.key).toBe('ipadapter_file')
// DepthAnything
const depthProvider = modelToNodeStore.getNodeProvider('depthanything')
expect(depthProvider?.nodeDef?.name).toBe(
'DownloadAndLoadDepthAnythingV2Model'
)
expect(depthProvider?.key).toBe('model')
})
it('should use hierarchical fallback for ultralytics subcategories', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
// ultralytics/bbox should fall back to ultralytics
const bboxProvider = modelToNodeStore.getNodeProvider('ultralytics/bbox')
expect(bboxProvider?.nodeDef?.name).toBe('UltralyticsDetectorProvider')
expect(bboxProvider?.key).toBe('model_name')
// ultralytics/segm should also fall back to ultralytics
const segmProvider = modelToNodeStore.getNodeProvider('ultralytics/segm')
expect(segmProvider?.nodeDef?.name).toBe('UltralyticsDetectorProvider')
})
it('should return provider for FlashVSR nodes with empty key (auto-load)', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
const flashVSRProvider = modelToNodeStore.getNodeProvider('FlashVSR')
expect(flashVSRProvider?.nodeDef?.name).toBe('FlashVSRNode')
expect(flashVSRProvider?.key).toBe('')
const flashVSR11Provider =
modelToNodeStore.getNodeProvider('FlashVSR-v1.1')
expect(flashVSR11Provider?.nodeDef?.name).toBe('FlashVSRNode')
expect(flashVSR11Provider?.key).toBe('')
})
it('should return provider for segformer models', () => {
const modelToNodeStore = useModelToNodeStore()
modelToNodeStore.registerDefaults()
const segformerB2Provider = modelToNodeStore.getNodeProvider(
'segformer_b2_clothes'
)
expect(segformerB2Provider?.nodeDef?.name).toBe('LS_LoadSegformerModel')
expect(segformerB2Provider?.key).toBe('model_name')
const segformerB3FashionProvider = modelToNodeStore.getNodeProvider(
'segformer_b3_fashion'
)
expect(segformerB3FashionProvider?.nodeDef?.name).toBe(
'LS_LoadSegformerModel'
)
})
const provider = modelToNodeStore.getNodeProvider(modelType)
expect(provider?.nodeDef?.name).toBe(expectedNodeName)
expect(provider?.key).toBe(expectedKey)
}
)
})
describe('getAllNodeProviders', () => {

View File

@@ -334,7 +334,7 @@ describe('useQueueStore', () => {
})
describe('update() - sorting', () => {
it('should sort tasks by queueIndex descending', async () => {
it('should sort tasks by job.priority descending', async () => {
const job1 = createHistoryJob(1, 'hist-1')
const job2 = createHistoryJob(5, 'hist-2')
const job3 = createHistoryJob(3, 'hist-3')
@@ -344,9 +344,9 @@ describe('useQueueStore', () => {
await store.update()
expect(store.historyTasks[0].queueIndex).toBe(5)
expect(store.historyTasks[1].queueIndex).toBe(3)
expect(store.historyTasks[2].queueIndex).toBe(1)
expect(store.historyTasks[0].job.priority).toBe(5)
expect(store.historyTasks[1].job.priority).toBe(3)
expect(store.historyTasks[2].job.priority).toBe(1)
})
it('should preserve API sort order for pending tasks', async () => {
@@ -363,14 +363,14 @@ describe('useQueueStore', () => {
await store.update()
expect(store.pendingTasks[0].queueIndex).toBe(15)
expect(store.pendingTasks[1].queueIndex).toBe(12)
expect(store.pendingTasks[2].queueIndex).toBe(10)
expect(store.pendingTasks[0].job.priority).toBe(15)
expect(store.pendingTasks[1].job.priority).toBe(12)
expect(store.pendingTasks[2].job.priority).toBe(10)
})
})
describe('update() - queue index collision (THE BUG FIX)', () => {
it('should NOT confuse different prompts with same queueIndex', async () => {
it('should NOT confuse different prompts with same job.priority', async () => {
const hist1 = createHistoryJob(50, 'prompt-uuid-aaa')
mockGetQueue.mockResolvedValue({ Running: [], Pending: [] })
@@ -387,10 +387,10 @@ describe('useQueueStore', () => {
expect(store.historyTasks).toHaveLength(1)
expect(store.historyTasks[0].jobId).toBe('prompt-uuid-bbb')
expect(store.historyTasks[0].queueIndex).toBe(51)
expect(store.historyTasks[0].job.priority).toBe(51)
})
it('should correctly reconcile when queueIndex is reused', async () => {
it('should correctly reconcile when job.priority is reused', async () => {
const hist1 = createHistoryJob(100, 'first-prompt-at-100')
const hist2 = createHistoryJob(99, 'prompt-at-99')
@@ -412,7 +412,7 @@ describe('useQueueStore', () => {
expect(jobIds).not.toContain('first-prompt-at-100')
})
it('should handle multiple queueIndex collisions simultaneously', async () => {
it('should handle multiple job.priority collisions simultaneously', async () => {
const hist1 = createHistoryJob(10, 'old-at-10')
const hist2 = createHistoryJob(20, 'old-at-20')
const hist3 = createHistoryJob(30, 'keep-at-30')
@@ -563,9 +563,9 @@ describe('useQueueStore', () => {
await store.update()
expect(store.historyTasks).toHaveLength(3)
expect(store.historyTasks[0].queueIndex).toBe(10)
expect(store.historyTasks[1].queueIndex).toBe(9)
expect(store.historyTasks[2].queueIndex).toBe(8)
expect(store.historyTasks[0].job.priority).toBe(10)
expect(store.historyTasks[1].job.priority).toBe(9)
expect(store.historyTasks[2].job.priority).toBe(8)
})
it('should respect maxHistoryItems when combining new and existing', async () => {
@@ -589,7 +589,7 @@ describe('useQueueStore', () => {
await store.update()
expect(store.historyTasks).toHaveLength(5)
expect(store.historyTasks[0].queueIndex).toBe(23)
expect(store.historyTasks[0].job.priority).toBe(23)
})
it('should handle maxHistoryItems = 0', async () => {
@@ -619,7 +619,7 @@ describe('useQueueStore', () => {
await store.update()
expect(store.historyTasks).toHaveLength(1)
expect(store.historyTasks[0].queueIndex).toBe(10)
expect(store.historyTasks[0].job.priority).toBe(10)
})
it('should dynamically adjust when maxHistoryItems changes', async () => {

View File

@@ -312,10 +312,6 @@ export class TaskItemImpl {
return this.jobId + this.displayStatus
}
get queueIndex() {
return this.job.priority
}
get jobId() {
return this.job.id
}
@@ -500,7 +496,7 @@ export const useQueueStore = defineStore('queue', () => {
)
const lastHistoryQueueIndex = computed<number>(() =>
historyTasks.value.length ? historyTasks.value[0].queueIndex : -1
historyTasks.value.length ? historyTasks.value[0].job.priority : -1
)
const hasPendingTasks = computed<boolean>(() => pendingTasks.value.length > 0)

View File

@@ -44,7 +44,7 @@ export const iconForJobState = (state: JobState): string => {
const buildTitle = (task: TaskItemImpl, t: (k: string) => string): string => {
const prefix = t('g.job')
const shortId = String(task.jobId ?? '').split('-')[0]
const idx = task.queueIndex
const idx = task.job.priority
if (typeof idx === 'number') return `${prefix} #${idx}`
if (shortId) return `${prefix} ${shortId}`
return prefix