mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-05 21:54:50 +00:00
Compare commits
19 Commits
FE-407-3d-
...
feat/toolt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25ac20ffb9 | ||
|
|
926d48f772 | ||
|
|
b593496e3e | ||
|
|
a9e53b997c | ||
|
|
3aef8da214 | ||
|
|
5b2f4b303a | ||
|
|
7bf2120cc5 | ||
|
|
8cd39fc24f | ||
|
|
1065c3da23 | ||
|
|
30e4b443d1 | ||
|
|
77105602c0 | ||
|
|
5e8146267a | ||
|
|
f8e2a81666 | ||
|
|
d429a10ea6 | ||
|
|
82a0b59367 | ||
|
|
f16214a719 | ||
|
|
ae07b4d3f8 | ||
|
|
2a0daf20da | ||
|
|
356a291d09 |
47
browser_tests/tests/baseTooltip.spec.ts
Normal file
47
browser_tests/tests/baseTooltip.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
function tooltipLocator(page: Page): Locator {
|
||||
return page.locator('[role="tooltip"]')
|
||||
}
|
||||
|
||||
async function hoverAway(page: Page): Promise<void> {
|
||||
await page.mouse.move(0, 0)
|
||||
}
|
||||
|
||||
test.describe('BaseTooltip regression', { tag: '@ui' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('Queue history button shows tooltip on hover', async ({ comfyPage }) => {
|
||||
const queueButton = comfyPage.page.getByTestId('queue-overlay-toggle')
|
||||
await queueButton.hover()
|
||||
|
||||
const tooltip = tooltipLocator(comfyPage.page)
|
||||
await expect(tooltip).toBeVisible()
|
||||
|
||||
await hoverAway(comfyPage.page)
|
||||
await expect(tooltip).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Toggle properties panel button shows tooltip on hover', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const panelButton = comfyPage.page
|
||||
.getByLabel(/Toggle properties panel/i)
|
||||
.first()
|
||||
await panelButton.hover()
|
||||
|
||||
const tooltip = tooltipLocator(comfyPage.page)
|
||||
await expect(tooltip).toBeVisible()
|
||||
|
||||
await hoverAway(comfyPage.page)
|
||||
await expect(tooltip).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<GlobalDialog />
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<TooltipProvider :delay-duration="300" disable-hoverable-content>
|
||||
<router-view />
|
||||
<GlobalDialog />
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { captureException } from '@sentry/vue'
|
||||
import BlockUI from 'primevue/blockui'
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
|
||||
@@ -121,6 +121,7 @@ function createWrapper({
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<slot />' },
|
||||
SubgraphBreadcrumb: true,
|
||||
QueueProgressOverlay: true,
|
||||
QueueInlineProgressSummary: true,
|
||||
@@ -134,9 +135,6 @@ function createWrapper({
|
||||
'<div data-testid="context-menu" :data-model="JSON.stringify(model)" />'
|
||||
},
|
||||
...stubs
|
||||
},
|
||||
directives: {
|
||||
tooltip: () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,23 @@
|
||||
v-if="managerState.shouldShowManagerButtons.value"
|
||||
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.bottom="customNodesManagerTooltipConfig"
|
||||
variant="secondary"
|
||||
:aria-label="t('menu.manageExtensions')"
|
||||
class="relative"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
<i class="icon-[comfy--extensions-blocks] size-4" />
|
||||
<span class="not-md:hidden">
|
||||
{{ t('menu.manageExtensions') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="shouldShowRedDot"
|
||||
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
||||
/>
|
||||
</Button>
|
||||
<BaseTooltip :text="t('menu.manageExtensions')" side="bottom">
|
||||
<Button
|
||||
variant="secondary"
|
||||
:aria-label="t('menu.manageExtensions')"
|
||||
class="relative"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
<i class="icon-[comfy--extensions-blocks] size-4" />
|
||||
<span class="not-md:hidden">
|
||||
{{ t('menu.manageExtensions') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="shouldShowRedDot"
|
||||
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
||||
/>
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
|
||||
<div ref="actionbarContainerRef" :class="actionbarContainerClass">
|
||||
@@ -53,35 +54,43 @@
|
||||
class="shrink-0"
|
||||
/>
|
||||
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
|
||||
<Button
|
||||
<BaseTooltip
|
||||
v-if="isCloud && flags.workflowSharingEnabled"
|
||||
v-tooltip.bottom="shareTooltipConfig"
|
||||
variant="secondary"
|
||||
:aria-label="t('actionbar.shareTooltip')"
|
||||
@click="() => openShareDialog().catch(toastErrorHandler)"
|
||||
@pointerenter="prefetchShareDialog"
|
||||
:text="t('actionbar.shareTooltip')"
|
||||
side="bottom"
|
||||
>
|
||||
<i class="icon-[comfy--send] size-4" />
|
||||
<span class="not-md:hidden">
|
||||
{{ t('actionbar.share') }}
|
||||
</span>
|
||||
</Button>
|
||||
<div v-if="!isRightSidePanelOpen" class="relative">
|
||||
<Button
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
:class="
|
||||
cn(
|
||||
showErrorIndicatorOnPanelButton &&
|
||||
'outline-1 outline-destructive-background'
|
||||
)
|
||||
"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
:aria-label="t('actionbar.shareTooltip')"
|
||||
@click="() => openShareDialog().catch(toastErrorHandler)"
|
||||
@pointerenter="prefetchShareDialog"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
<i class="icon-[comfy--send] size-4" />
|
||||
<span class="not-md:hidden">
|
||||
{{ t('actionbar.share') }}
|
||||
</span>
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
<div v-if="!isRightSidePanelOpen" class="relative">
|
||||
<BaseTooltip
|
||||
:text="t('rightSidePanel.togglePanel')"
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
:class="
|
||||
cn(
|
||||
showErrorIndicatorOnPanelButton &&
|
||||
'outline-1 outline-destructive-background'
|
||||
)
|
||||
"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
<StatusBadge
|
||||
v-if="showErrorIndicatorOnPanelButton"
|
||||
variant="dot"
|
||||
@@ -146,7 +155,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
@@ -251,12 +260,6 @@ const inlineProgressSummaryTarget = computed(() => {
|
||||
const shouldHideInlineProgressSummary = computed(
|
||||
() => isQueueProgressOverlayEnabled.value && isQueueOverlayExpanded.value
|
||||
)
|
||||
const customNodesManagerTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.manageExtensions'))
|
||||
)
|
||||
const shareTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('actionbar.shareTooltip'))
|
||||
)
|
||||
|
||||
const shouldShowRedDot = computed((): boolean => {
|
||||
return shouldShowConflictRedDot.value
|
||||
@@ -278,9 +281,6 @@ const showErrorIndicatorOnPanelButton = computed(
|
||||
|
||||
// Right side panel toggle
|
||||
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
|
||||
const rightSidePanelTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('rightSidePanel.togglePanel'))
|
||||
)
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
|
||||
@@ -36,6 +36,7 @@ const renderActionbar = (showRunProgressBar: boolean) => {
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<slot />' },
|
||||
ContextMenu: {
|
||||
name: 'ContextMenu',
|
||||
template: '<div />'
|
||||
@@ -50,9 +51,6 @@ const renderActionbar = (showRunProgressBar: boolean) => {
|
||||
template: '<button type="button">Run</button>'
|
||||
},
|
||||
QueueInlineProgress: true
|
||||
},
|
||||
directives: {
|
||||
tooltip: () => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,47 +32,49 @@
|
||||
<Suspense @resolve="comfyRunButtonResolved">
|
||||
<ComfyRunButton />
|
||||
</Suspense>
|
||||
<Button
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
:aria-pressed="
|
||||
isQueuePanelV2Enabled
|
||||
? activeSidebarTabId === 'job-history'
|
||||
: queueOverlayExpanded
|
||||
"
|
||||
class="relative px-3"
|
||||
data-testid="queue-overlay-toggle"
|
||||
@click="toggleQueueOverlay"
|
||||
@contextmenu.stop.prevent="showQueueContextMenu"
|
||||
>
|
||||
<span class="text-sm font-normal tabular-nums">
|
||||
{{ activeJobsLabel }}
|
||||
</span>
|
||||
<StatusBadge
|
||||
v-if="activeJobsCount > 0"
|
||||
data-testid="active-jobs-indicator"
|
||||
variant="dot"
|
||||
class="pointer-events-none absolute -top-0.5 -right-0.5 animate-pulse"
|
||||
/>
|
||||
<span class="sr-only">
|
||||
{{
|
||||
<BaseTooltip :text="t('menu.interrupt')" side="bottom">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip :text="queueHistoryTooltipText" side="bottom">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
:aria-pressed="
|
||||
isQueuePanelV2Enabled
|
||||
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
|
||||
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
}}
|
||||
</span>
|
||||
</Button>
|
||||
? activeSidebarTabId === 'job-history'
|
||||
: queueOverlayExpanded
|
||||
"
|
||||
class="relative px-3"
|
||||
data-testid="queue-overlay-toggle"
|
||||
@click="toggleQueueOverlay"
|
||||
@contextmenu.stop.prevent="showQueueContextMenu"
|
||||
>
|
||||
<span class="text-sm font-normal tabular-nums">
|
||||
{{ activeJobsLabel }}
|
||||
</span>
|
||||
<StatusBadge
|
||||
v-if="activeJobsCount > 0"
|
||||
data-testid="active-jobs-indicator"
|
||||
variant="dot"
|
||||
class="pointer-events-none absolute -top-0.5 -right-0.5 animate-pulse"
|
||||
/>
|
||||
<span class="sr-only">
|
||||
{{
|
||||
isQueuePanelV2Enabled
|
||||
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
|
||||
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
}}
|
||||
</span>
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" />
|
||||
</div>
|
||||
</Panel>
|
||||
@@ -108,7 +110,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -363,16 +365,11 @@ watch(isDragging, (dragging) => {
|
||||
}
|
||||
})
|
||||
|
||||
const cancelJobTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.interrupt'))
|
||||
)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(
|
||||
t(
|
||||
isQueuePanelV2Enabled.value
|
||||
? 'sideToolbar.queueProgressOverlay.viewJobHistory'
|
||||
: 'sideToolbar.queueProgressOverlay.expandCollapsedQueue'
|
||||
)
|
||||
const queueHistoryTooltipText = computed(() =>
|
||||
t(
|
||||
isQueuePanelV2Enabled.value
|
||||
? 'sideToolbar.queueProgressOverlay.viewJobHistory'
|
||||
: 'sideToolbar.queueProgressOverlay.expandCollapsedQueue'
|
||||
)
|
||||
)
|
||||
const activeJobsLabel = computed(() => {
|
||||
|
||||
@@ -54,11 +54,17 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
|
||||
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
|
||||
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
const renderMenu = () =>
|
||||
render(JobHistoryActionsMenu, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => {} }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -117,7 +123,9 @@ describe('JobHistoryActionsMenu', () => {
|
||||
props: { onClearHistory: clearHistorySpy },
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => {} }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,91 +1,94 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-1">
|
||||
<Popover :show-arrow="false">
|
||||
<template #button>
|
||||
<Button
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<div class="flex min-w-56 flex-col items-stretch font-inter">
|
||||
<BaseTooltip :text="t('g.more')" side="top">
|
||||
<div class="flex items-center gap-1">
|
||||
<Popover :show-arrow="false">
|
||||
<template #button>
|
||||
<Button
|
||||
data-testid="docked-job-history-action"
|
||||
class="w-full justify-between text-sm font-light"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onToggleDockedJobHistory(close)"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<i
|
||||
class="icon-[lucide--panel-left-close] size-4 text-text-secondary"
|
||||
/>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.dockedJobHistory')
|
||||
}}</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
class="icon-[lucide--check] size-4"
|
||||
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="show-run-progress-bar-action"
|
||||
class="w-full justify-between text-sm font-light"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onToggleRunProgressBar"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="icon-[lucide--hourglass] size-4 text-text-secondary" />
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.showRunProgressBar')
|
||||
}}</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isRunProgressBarEnabled"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<!-- TODO: Bug in assets sidebar panel derives assets from history, so despite this not deleting the assets, it still effectively shows to the user as deleted -->
|
||||
<template v-if="showClearHistoryAction">
|
||||
<div class="my-1 border-t border-interface-stroke" />
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<div class="flex min-w-56 flex-col items-stretch font-inter">
|
||||
<Button
|
||||
data-testid="clear-history-action"
|
||||
class="h-auto min-h-8 w-full items-start justify-start whitespace-normal"
|
||||
data-testid="docked-job-history-action"
|
||||
class="w-full justify-between text-sm font-light"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onClearHistoryFromMenu(close)"
|
||||
@click="onToggleDockedJobHistory(close)"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--trash-2] size-4 shrink-0 self-center text-destructive-background"
|
||||
/>
|
||||
<span
|
||||
class="flex flex-col items-start text-left leading-tight wrap-break-word"
|
||||
>
|
||||
<span class="text-sm font-light">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearHistory') }}
|
||||
</span>
|
||||
<span class="text-xs font-light text-text-secondary">
|
||||
{{
|
||||
t(
|
||||
'sideToolbar.queueProgressOverlay.clearHistoryMenuAssetsNote'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<i
|
||||
class="icon-[lucide--panel-left-close] size-4 text-text-secondary"
|
||||
/>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.dockedJobHistory')
|
||||
}}</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
<Button
|
||||
data-testid="show-run-progress-bar-action"
|
||||
class="w-full justify-between text-sm font-light"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onToggleRunProgressBar"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<i
|
||||
class="icon-[lucide--hourglass] size-4 text-text-secondary"
|
||||
/>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.showRunProgressBar')
|
||||
}}</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isRunProgressBarEnabled"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<!-- TODO: Bug in assets sidebar panel derives assets from history, so despite this not deleting the assets, it still effectively shows to the user as deleted -->
|
||||
<template v-if="showClearHistoryAction">
|
||||
<div class="my-1 border-t border-interface-stroke" />
|
||||
<Button
|
||||
data-testid="clear-history-action"
|
||||
class="h-auto min-h-8 w-full items-start justify-start whitespace-normal"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onClearHistoryFromMenu(close)"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--trash-2] size-4 shrink-0 self-center text-destructive-background"
|
||||
/>
|
||||
<span
|
||||
class="flex flex-col items-start text-left leading-tight wrap-break-word"
|
||||
>
|
||||
<span class="text-sm font-light">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearHistory') }}
|
||||
</span>
|
||||
<span class="text-xs font-light text-text-secondary">
|
||||
{{
|
||||
t(
|
||||
'sideToolbar.queueProgressOverlay.clearHistoryMenuAssetsNote'
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</BaseTooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -95,7 +98,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
@@ -108,7 +111,6 @@ const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
|
||||
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
|
||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||
useQueueFeatureFlags()
|
||||
const showClearHistoryAction = computed(() => !isCloud)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayActive from './QueueOverlayActive.vue'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -28,9 +27,8 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
const tooltipDirectiveStub = {
|
||||
mounted: vi.fn(),
|
||||
updated: vi.fn()
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
@@ -49,8 +47,8 @@ const renderComponent = (props: Record<string, unknown> = {}) =>
|
||||
props: { ...defaultProps, ...props },
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: {
|
||||
tooltip: tooltipDirectiveStub
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -111,13 +109,4 @@ describe('QueueOverlayActive', () => {
|
||||
screen.queryByRole('button', { name: 'Clear queued' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('builds tooltip configs with translated strings', () => {
|
||||
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
|
||||
|
||||
renderComponent()
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('Cancel job')
|
||||
expect(spy).toHaveBeenCalledWith('Clear queue')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -42,18 +42,22 @@
|
||||
t('sideToolbar.queueProgressOverlay.running')
|
||||
}}</span>
|
||||
</span>
|
||||
<Button
|
||||
<BaseTooltip
|
||||
v-if="runningCount > 0"
|
||||
v-tooltip.top="cancelJobTooltip"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
||||
@click="$emit('interruptAll')"
|
||||
:text="t('sideToolbar.queueProgressOverlay.cancelJobTooltip')"
|
||||
side="top"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
||||
@click="$emit('interruptAll')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -63,18 +67,22 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</span>
|
||||
<Button
|
||||
<BaseTooltip
|
||||
v-if="queuedCount > 0"
|
||||
v-tooltip.top="clearQueueTooltip"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
:text="t('sideToolbar.queueProgressOverlay.clearQueueTooltip')"
|
||||
side="top"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,11 +99,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
|
||||
defineProps<{
|
||||
totalProgressStyle: Record<string, string>
|
||||
@@ -115,10 +122,4 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const cancelJobTooltip = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.cancelJobTooltip'))
|
||||
)
|
||||
const clearQueueTooltip = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearQueueTooltip'))
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -49,11 +49,9 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
}))
|
||||
|
||||
import QueueOverlayHeader from './QueueOverlayHeader.vue'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
|
||||
const tooltipDirectiveStub = {
|
||||
mounted: vi.fn(),
|
||||
updated: vi.fn()
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
const renderHeader = (props = {}) =>
|
||||
@@ -65,7 +63,9 @@ const renderHeader = (props = {}) =>
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: tooltipDirectiveStub }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -108,7 +108,6 @@ describe('QueueOverlayHeader', () => {
|
||||
|
||||
it('emits clear history from the menu', async () => {
|
||||
const user = userEvent.setup()
|
||||
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
|
||||
const clearHistorySpy = vi.fn()
|
||||
|
||||
renderHeader({ onClearHistory: clearHistorySpy })
|
||||
@@ -116,7 +115,6 @@ describe('QueueOverlayHeader', () => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'More options' })
|
||||
).toBeInTheDocument()
|
||||
expect(spy).toHaveBeenCalledWith('More')
|
||||
|
||||
await user.click(screen.getByTestId('clear-history-action'))
|
||||
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -11,28 +11,31 @@
|
||||
<span :class="{ 'opacity-50': queuedCount === 0 }">{{
|
||||
t('sideToolbar.queueProgressOverlay.clearQueueTooltip')
|
||||
}}</span>
|
||||
<Button
|
||||
v-tooltip.top="clearAllJobsTooltip"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
:disabled="queuedCount === 0"
|
||||
@click="$emit('clearQueued')"
|
||||
<BaseTooltip
|
||||
:text="t('sideToolbar.queueProgressOverlay.clearAllJobsTooltip')"
|
||||
side="top"
|
||||
>
|
||||
<i class="icon-[lucide--list-x] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
:disabled="queuedCount === 0"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i class="icon-[lucide--list-x] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<JobHistoryActionsMenu @clear-history="$emit('clearHistory')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
|
||||
defineProps<{
|
||||
headerTitle: string
|
||||
@@ -45,7 +48,4 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const clearAllJobsTooltip = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearAllJobsTooltip'))
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -11,103 +11,119 @@
|
||||
class="flex shrink-0 items-center gap-2"
|
||||
:class="{ 'ml-2': !showSearch }"
|
||||
>
|
||||
<Popover :show-arrow="false">
|
||||
<template #button>
|
||||
<Button
|
||||
v-tooltip.top="filterTooltipConfig"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
|
||||
>
|
||||
<i class="icon-[lucide--list-filter] size-4" />
|
||||
<span
|
||||
v-if="selectedWorkflowFilter !== 'all'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<div class="flex min-w-48 flex-col items-stretch">
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onSelectWorkflowFilter('all', close)"
|
||||
>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
||||
}}</span>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'all'"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<div class="mx-2 mt-1 h-px" />
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onSelectWorkflowFilter('current', close)"
|
||||
>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
||||
}}</span>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'current'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<Popover :show-arrow="false">
|
||||
<template #button>
|
||||
<Button
|
||||
v-tooltip.top="sortTooltipConfig"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
|
||||
>
|
||||
<i class="icon-[lucide--arrow-up-down] size-4" />
|
||||
<span
|
||||
v-if="selectedSortMode !== 'mostRecent'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<div class="flex min-w-48 flex-col items-stretch">
|
||||
<template v-for="(mode, index) in jobSortModes" :key="mode">
|
||||
<BaseTooltip
|
||||
:text="t('sideToolbar.queueProgressOverlay.filterBy')"
|
||||
side="top"
|
||||
>
|
||||
<div>
|
||||
<Popover :show-arrow="false">
|
||||
<template #button>
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onSelectSortMode(mode, close)"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
|
||||
>
|
||||
<span>{{ sortLabel(mode) }}</span>
|
||||
<i
|
||||
v-if="selectedSortMode === mode"
|
||||
class="icon-[lucide--check] size-4 text-text-secondary"
|
||||
<i class="icon-[lucide--list-filter] size-4" />
|
||||
<span
|
||||
v-if="selectedWorkflowFilter !== 'all'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-if="index < jobSortModes.length - 1"
|
||||
class="mx-2 mt-1 h-px"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
<Button
|
||||
v-if="showAssetsAction"
|
||||
v-tooltip.top="showAssetsTooltipConfig"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.showAssetsPanel')"
|
||||
@click="emit('showAssets')"
|
||||
<template #default="{ close }">
|
||||
<div class="flex min-w-48 flex-col items-stretch">
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onSelectWorkflowFilter('all', close)"
|
||||
>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
||||
}}</span>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'all'"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<div class="mx-2 mt-1 h-px" />
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onSelectWorkflowFilter('current', close)"
|
||||
>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
||||
}}</span>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'current'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip
|
||||
:text="t('sideToolbar.queueProgressOverlay.sortBy')"
|
||||
side="top"
|
||||
>
|
||||
<i class="icon-[comfy--image-ai-edit] size-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<Popover :show-arrow="false">
|
||||
<template #button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
|
||||
>
|
||||
<i class="icon-[lucide--arrow-up-down] size-4" />
|
||||
<span
|
||||
v-if="selectedSortMode !== 'mostRecent'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<div class="flex min-w-48 flex-col items-stretch">
|
||||
<template v-for="(mode, index) in jobSortModes" :key="mode">
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click="onSelectSortMode(mode, close)"
|
||||
>
|
||||
<span>{{ sortLabel(mode) }}</span>
|
||||
<i
|
||||
v-if="selectedSortMode === mode"
|
||||
class="icon-[lucide--check] size-4 text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-if="index < jobSortModes.length - 1"
|
||||
class="mx-2 mt-1 h-px"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip
|
||||
v-if="showAssetsAction"
|
||||
:text="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
side="top"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.showAssetsPanel')"
|
||||
@click="emit('showAssets')"
|
||||
>
|
||||
<i class="icon-[comfy--image-ai-edit] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,7 +137,7 @@ import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { jobSortModes } from '@/composables/queue/useJobList'
|
||||
import type { JobSortMode } from '@/composables/queue/useJobList'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
|
||||
const {
|
||||
hideShowAssetsAction = false,
|
||||
@@ -148,15 +164,6 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const filterTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.filterBy'))
|
||||
)
|
||||
const sortTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.sortBy'))
|
||||
)
|
||||
const showAssetsTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.showAssets'))
|
||||
)
|
||||
const showAssetsAction = computed(() => !hideShowAssetsAction)
|
||||
const searchPlaceholderText = computed(
|
||||
() => searchPlaceholder ?? t('sideToolbar.queueProgressOverlay.searchJobs')
|
||||
|
||||
@@ -55,6 +55,10 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
describe('JobFiltersBar', () => {
|
||||
it('emits showAssets when the assets icon button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
@@ -70,7 +74,9 @@ describe('JobFiltersBar', () => {
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => undefined }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -90,7 +96,9 @@ describe('JobFiltersBar', () => {
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => undefined }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -124,30 +124,38 @@
|
||||
key="actions"
|
||||
class="inline-flex items-center gap-2 pr-1"
|
||||
>
|
||||
<Button
|
||||
<BaseTooltip
|
||||
v-if="state === 'failed' && computedShowClear"
|
||||
v-tooltip.top="deleteTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.delete')"
|
||||
@click.stop="onDeleteClick"
|
||||
:text="t('g.delete')"
|
||||
side="top"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.delete')"
|
||||
@click.stop="onDeleteClick"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip
|
||||
v-else-if="
|
||||
state !== 'completed' &&
|
||||
state !== 'running' &&
|
||||
computedShowClear
|
||||
"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="onCancelClick"
|
||||
:text="t('g.cancel')"
|
||||
side="top"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="onCancelClick"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
<Button
|
||||
v-else-if="state === 'completed'"
|
||||
variant="textonly"
|
||||
@@ -155,32 +163,40 @@
|
||||
@click.stop="emit('view')"
|
||||
>{{ t('menuLabels.View') }}</Button
|
||||
>
|
||||
<Button
|
||||
<BaseTooltip
|
||||
v-if="showMenu !== undefined ? showMenu : true"
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
variant="textonly"
|
||||
size="icon-sm"
|
||||
:aria-label="t('g.more')"
|
||||
@click.stop="emit('menu', $event)"
|
||||
:text="t('g.more')"
|
||||
side="top"
|
||||
>
|
||||
<i class="icon-[lucide--more-horizontal] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="icon-sm"
|
||||
:aria-label="t('g.more')"
|
||||
@click.stop="emit('menu', $event)"
|
||||
>
|
||||
<i class="icon-[lucide--more-horizontal] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
<div v-else-if="state !== 'running'" key="secondary" class="pr-2">
|
||||
<slot name="secondary">{{ rightText }}</slot>
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- Running job cancel button - always visible -->
|
||||
<Button
|
||||
<BaseTooltip
|
||||
v-if="state === 'running' && computedShowClear"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="onCancelClick"
|
||||
:text="t('g.cancel')"
|
||||
side="top"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="onCancelClick"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,7 +211,7 @@ import { getHoverPopoverPosition } from '@/components/queue/job/getHoverPopoverP
|
||||
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -247,10 +263,6 @@ const {
|
||||
progressPercentStyle
|
||||
} = useProgressBarBackground()
|
||||
|
||||
const cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel')))
|
||||
const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))
|
||||
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const showDetails = computed(() => activeDetailsId === jobId)
|
||||
|
||||
|
||||
189
src/components/ui/tooltip/BaseTooltip.stories.ts
Normal file
189
src/components/ui/tooltip/BaseTooltip.stories.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { TooltipProvider } from 'reka-ui'
|
||||
|
||||
import BaseTooltip from './BaseTooltip.vue'
|
||||
import { FOR_STORIES } from './tooltip.variants'
|
||||
|
||||
const { sizes, sides } = FOR_STORIES
|
||||
|
||||
const meta: Meta<typeof BaseTooltip> = {
|
||||
title: 'Components/Tooltip/BaseTooltip',
|
||||
component: BaseTooltip,
|
||||
tags: ['autodocs'],
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { TooltipProvider, story },
|
||||
template:
|
||||
'<TooltipProvider :delay-duration="0"><div class="flex items-center justify-center p-20"><story /></div></TooltipProvider>'
|
||||
})
|
||||
],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: sizes
|
||||
},
|
||||
side: {
|
||||
control: { type: 'select' },
|
||||
options: sides
|
||||
},
|
||||
text: { control: 'text' },
|
||||
keybind: { control: 'text' },
|
||||
showIcon: { control: 'boolean' },
|
||||
disabled: { control: 'boolean' }
|
||||
},
|
||||
args: {
|
||||
size: 'small',
|
||||
side: 'top',
|
||||
text: 'Tooltip text',
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Small: Story = {
|
||||
render: (args) => ({
|
||||
components: { BaseTooltip },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<BaseTooltip v-bind="args">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
Hover me
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
})
|
||||
}
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: 'large',
|
||||
text: 'This is a longer tooltip that can wrap to multiple lines for detailed descriptions of node functionality.'
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BaseTooltip },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<BaseTooltip v-bind="args">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
Hover me
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
})
|
||||
}
|
||||
|
||||
export const WithKeybind: Story = {
|
||||
args: {
|
||||
text: 'Undo',
|
||||
keybind: 'Ctrl+Z'
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BaseTooltip },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<BaseTooltip v-bind="args">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
Hover me
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
})
|
||||
}
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
text: 'More options',
|
||||
showIcon: true,
|
||||
size: 'small'
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BaseTooltip },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<BaseTooltip v-bind="args">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
Hover me
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
})
|
||||
}
|
||||
|
||||
export const WithKeybindAndIcon: Story = {
|
||||
args: {
|
||||
text: 'Save',
|
||||
keybind: 'Ctrl+S',
|
||||
showIcon: true,
|
||||
size: 'small'
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BaseTooltip },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<BaseTooltip v-bind="args">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
Hover me
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
})
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
text: 'This tooltip is disabled',
|
||||
disabled: true
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BaseTooltip },
|
||||
setup: () => ({ args }),
|
||||
template: `
|
||||
<BaseTooltip v-bind="args">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
Hover me (disabled tooltip)
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllSides: Story = {
|
||||
render: () => ({
|
||||
components: { BaseTooltip },
|
||||
template: `
|
||||
<div class="grid grid-cols-2 gap-12">
|
||||
${sides
|
||||
.map(
|
||||
(side) => `
|
||||
<BaseTooltip text="${side} tooltip" side="${side}" size="small">
|
||||
<button class="w-full rounded-lg bg-secondary-background px-4 py-2 text-sm">
|
||||
${side}
|
||||
</button>
|
||||
</BaseTooltip>`
|
||||
)
|
||||
.join('\n')}
|
||||
</div>`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { BaseTooltip },
|
||||
template: `
|
||||
<div class="flex flex-col gap-12">
|
||||
<div class="flex flex-wrap items-center gap-8">
|
||||
<BaseTooltip text="Small tooltip" size="small" side="bottom">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">Small</button>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip text="This is a large tooltip with longer text that wraps across multiple lines." size="large" side="bottom">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">Large</button>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip text="Undo" keybind="Ctrl+Z" size="small" side="bottom">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">With Keybind</button>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip text="More options" :show-icon="true" size="small" side="bottom">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">With Icon</button>
|
||||
</BaseTooltip>
|
||||
<BaseTooltip text="Save" keybind="Ctrl+S" :show-icon="true" size="small" side="bottom">
|
||||
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">All Features</button>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
}
|
||||
75
src/components/ui/tooltip/BaseTooltip.vue
Normal file
75
src/components/ui/tooltip/BaseTooltip.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import {
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipRoot,
|
||||
TooltipTrigger
|
||||
} from 'reka-ui'
|
||||
|
||||
import type { TooltipVariants } from '@/components/ui/tooltip/tooltip.variants'
|
||||
import { tooltipVariants } from '@/components/ui/tooltip/tooltip.variants'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const {
|
||||
text = '',
|
||||
side = 'top',
|
||||
sideOffset = 4,
|
||||
size = 'small',
|
||||
keybind,
|
||||
showIcon = false,
|
||||
delayDuration,
|
||||
disabled = false,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
text?: string
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
sideOffset?: number
|
||||
size?: NonNullable<TooltipVariants['size']>
|
||||
keybind?: string
|
||||
showIcon?: boolean
|
||||
delayDuration?: number
|
||||
disabled?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipRoot :delay-duration="delayDuration" :disabled="disabled || !text">
|
||||
<TooltipTrigger as-child>
|
||||
<slot />
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
:side="side"
|
||||
:side-offset="sideOffset"
|
||||
:class="cn(tooltipVariants({ size }), className)"
|
||||
>
|
||||
<span
|
||||
v-if="keybind || (showIcon && size === 'small')"
|
||||
class="inline-flex items-center gap-2"
|
||||
>
|
||||
<span>{{ text }}</span>
|
||||
<i
|
||||
v-if="showIcon && size === 'small'"
|
||||
class="icon-[lucide--chevron-right] size-4 shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="keybind"
|
||||
class="shrink-0 rounded-sm bg-interface-menu-keybind-surface-default px-1 text-xs leading-none"
|
||||
>
|
||||
{{ keybind }}
|
||||
</span>
|
||||
</span>
|
||||
<template v-else>{{ text }}</template>
|
||||
<TooltipArrow
|
||||
:width="8"
|
||||
:height="5"
|
||||
class="fill-node-component-tooltip-surface stroke-node-component-tooltip-border"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
</template>
|
||||
24
src/components/ui/tooltip/tooltip.variants.ts
Normal file
24
src/components/ui/tooltip/tooltip.variants.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { VariantProps } from 'cva'
|
||||
import { cva } from 'cva'
|
||||
|
||||
export const tooltipVariants = cva({
|
||||
base: 'z-[1700] select-none border border-node-component-tooltip-border bg-node-component-tooltip-surface px-4 py-2 text-node-component-tooltip shadow-interface',
|
||||
variants: {
|
||||
size: {
|
||||
small: 'rounded-lg text-xs leading-none',
|
||||
large: 'max-w-75 rounded-sm text-sm/tight font-normal'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'small'
|
||||
}
|
||||
})
|
||||
|
||||
export type TooltipVariants = VariantProps<typeof tooltipVariants>
|
||||
|
||||
const sizes = ['small', 'large'] as const satisfies Array<
|
||||
TooltipVariants['size']
|
||||
>
|
||||
const sides = ['top', 'bottom', 'left', 'right'] as const
|
||||
|
||||
export const FOR_STORIES = { sizes, sides } as const
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Build a tooltip configuration object compatible with v-tooltip.
|
||||
* Consumers pass the translated text value.
|
||||
*/
|
||||
export const buildTooltipConfig = (value: string) => ({
|
||||
value,
|
||||
showDelay: 300,
|
||||
hideDelay: 0,
|
||||
pt: {
|
||||
text: {
|
||||
class:
|
||||
'border-node-component-tooltip-border bg-node-component-tooltip-surface text-node-component-tooltip border rounded-md px-2 py-1 text-xs leading-none shadow-none'
|
||||
},
|
||||
arrow: {
|
||||
class: 'border-t-node-component-tooltip-border'
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -46,12 +46,11 @@
|
||||
onThumbnailError($event.name, $event.previewUrl)
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-tooltip="buildTooltipConfig(item.name)"
|
||||
class="truncate text-xs text-base-foreground"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<BaseTooltip :text="item.name">
|
||||
<span class="truncate text-xs text-base-foreground">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</BaseTooltip>
|
||||
<span
|
||||
v-if="item.in_library"
|
||||
class="ml-auto shrink-0 text-xs text-muted-foreground"
|
||||
@@ -77,7 +76,7 @@ import ShareAssetThumbnail from '@/platform/workflow/sharing/components/ShareAss
|
||||
import { useAssetSections } from '@/platform/workflow/sharing/composables/useAssetSections'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
|
||||
const { items } = defineProps<{
|
||||
items: AssetInfo[]
|
||||
|
||||
@@ -54,7 +54,10 @@ describe(ShareAssetWarningBox, () => {
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<slot />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ describe('ShareWorkflowDialogContent', () => {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<slot />' },
|
||||
ComfyHubPublishIntroPanel: {
|
||||
template:
|
||||
'<section data-testid="publish-intro"><button data-testid="publish-intro-cta" @click="$props.onCreateProfile()">Start publishing</button></section>',
|
||||
|
||||
@@ -1,63 +1,71 @@
|
||||
<template>
|
||||
<div v-if="renderError" class="node-error p-1 text-xs text-red-500">⚠️</div>
|
||||
<div
|
||||
<BaseTooltip
|
||||
v-else
|
||||
v-tooltip.left="tooltipConfig"
|
||||
:class="
|
||||
cn(
|
||||
'lg-slot lg-slot--input group m-0 flex items-center rounded-r-lg',
|
||||
'cursor-crosshair',
|
||||
dotOnly ? 'lg-slot--dot-only' : 'pr-6',
|
||||
{
|
||||
'lg-slot--connected': props.connected,
|
||||
'lg-slot--compatible': props.compatible,
|
||||
'opacity-40': shouldDim
|
||||
},
|
||||
props.socketless && 'pointer-events-none invisible'
|
||||
)
|
||||
"
|
||||
:text="inputTooltipText"
|
||||
side="left"
|
||||
size="large"
|
||||
:delay-duration="tooltipDelay"
|
||||
:disabled="!tooltipsEnabled"
|
||||
>
|
||||
<!-- Connection Dot -->
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'w-3 -translate-x-1/2',
|
||||
hasError &&
|
||||
'before:pointer-events-none before:absolute before:size-4 before:rounded-full before:ring-2 before:ring-error before:ring-offset-0'
|
||||
'lg-slot lg-slot--input group m-0 flex items-center rounded-r-lg',
|
||||
'cursor-crosshair',
|
||||
dotOnly ? 'lg-slot--dot-only' : 'pr-6',
|
||||
{
|
||||
'lg-slot--connected': props.connected,
|
||||
'lg-slot--compatible': props.compatible,
|
||||
'opacity-40': shouldDim
|
||||
},
|
||||
props.socketless && 'pointer-events-none invisible'
|
||||
)
|
||||
"
|
||||
:slot-data
|
||||
@click="onClick"
|
||||
@dblclick="onDoubleClick"
|
||||
@pointerdown="onPointerDown"
|
||||
/>
|
||||
|
||||
<!-- Slot Name -->
|
||||
<div class="flex h-full min-w-0 items-center">
|
||||
<span
|
||||
v-if="!props.dotOnly && !hasNoLabel"
|
||||
>
|
||||
<!-- Connection Dot -->
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
:class="
|
||||
cn(
|
||||
'truncate text-node-component-slot-text',
|
||||
hasError && 'font-medium text-error'
|
||||
'w-3 -translate-x-1/2',
|
||||
hasError &&
|
||||
'before:pointer-events-none before:absolute before:size-4 before:rounded-full before:ring-2 before:ring-error before:ring-offset-0'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
slotData.label ||
|
||||
slotData.localized_name ||
|
||||
(slotData.name ?? `Input ${index}`)
|
||||
}}
|
||||
</span>
|
||||
:slot-data
|
||||
@click="onClick"
|
||||
@dblclick="onDoubleClick"
|
||||
@pointerdown="onPointerDown"
|
||||
/>
|
||||
|
||||
<!-- Slot Name -->
|
||||
<div class="flex h-full min-w-0 items-center">
|
||||
<span
|
||||
v-if="!props.dotOnly && !hasNoLabel"
|
||||
:class="
|
||||
cn(
|
||||
'truncate text-node-component-slot-text',
|
||||
hasError && 'font-medium text-error'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
slotData.label ||
|
||||
slotData.localized_name ||
|
||||
(slotData.name ?? `Input ${index}`)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseTooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
|
||||
@@ -94,15 +102,13 @@ const dotOnly = computed(() => props.dotOnly || hasNoLabel.value)
|
||||
const renderError = ref<string | null>(null)
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
const { getInputSlotTooltip, createTooltipConfig } = useNodeTooltips(
|
||||
const { getInputSlotTooltip, tooltipsEnabled, tooltipDelay } = useNodeTooltips(
|
||||
props.nodeType || ''
|
||||
)
|
||||
|
||||
const tooltipConfig = computed(() => {
|
||||
const inputTooltipText = computed(() => {
|
||||
const slotName = props.slotData.localized_name || props.slotData.name || ''
|
||||
const tooltipText = getInputSlotTooltip(slotName)
|
||||
const fallbackText = tooltipText || `Input: ${slotName}`
|
||||
return createTooltipConfig(fallbackText)
|
||||
return getInputSlotTooltip(slotName) || `Input: ${slotName}`
|
||||
})
|
||||
|
||||
onErrorCaptured((error) => {
|
||||
|
||||
@@ -199,6 +199,7 @@ describe('LGraphNode', () => {
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<div><slot /></div>' },
|
||||
NodeSlots: true,
|
||||
NodeWidgets: true,
|
||||
NodeContent: true,
|
||||
|
||||
@@ -94,26 +94,21 @@ const createGlobalConfig = () => {
|
||||
|
||||
const { pinia } = setupMockStores()
|
||||
|
||||
const tooltipDirective = {
|
||||
mounted: vi.fn(),
|
||||
updated: vi.fn(),
|
||||
unmounted: vi.fn()
|
||||
}
|
||||
|
||||
return {
|
||||
tooltipDirective,
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n, pinia],
|
||||
components: { InputText },
|
||||
directives: {
|
||||
tooltip: tooltipDirective
|
||||
stubs: {
|
||||
BaseTooltip: {
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderHeader = (props?: Partial<ComponentProps<typeof NodeHeader>>) => {
|
||||
const { global, tooltipDirective } = createGlobalConfig()
|
||||
const { global } = createGlobalConfig()
|
||||
const onCollapse = vi.fn()
|
||||
const onUpdateTitle = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
@@ -129,7 +124,7 @@ const renderHeader = (props?: Partial<ComponentProps<typeof NodeHeader>>) => {
|
||||
}
|
||||
})
|
||||
|
||||
return { ...result, user, onCollapse, onUpdateTitle, tooltipDirective }
|
||||
return { ...result, user, onCollapse, onUpdateTitle }
|
||||
}
|
||||
|
||||
describe('NodeHeader.vue', () => {
|
||||
@@ -204,47 +199,12 @@ describe('NodeHeader.vue', () => {
|
||||
})
|
||||
|
||||
describe('Tooltips', () => {
|
||||
it('applies tooltip directive to node title with correct configuration', () => {
|
||||
const { tooltipDirective } = renderHeader({
|
||||
it('renders node title inside a tooltip wrapper', () => {
|
||||
renderHeader({
|
||||
nodeData: makeNodeData({ type: 'KSampler' })
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('node-title')).toBeInTheDocument()
|
||||
expect(tooltipDirective.mounted).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('disables tooltip when editing is active', async () => {
|
||||
const { tooltipDirective } = renderHeader({
|
||||
nodeData: makeNodeData({ type: 'KSampler' })
|
||||
})
|
||||
|
||||
tooltipDirective.updated.mockClear()
|
||||
|
||||
// eslint-disable-next-line testing-library/prefer-user-event
|
||||
await fireEvent.dblClick(screen.getByTestId('node-header-1'))
|
||||
|
||||
expect(tooltipDirective.updated).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('creates tooltip configuration when component mounts', () => {
|
||||
const { tooltipDirective } = renderHeader({
|
||||
nodeData: makeNodeData({ type: 'KSampler' })
|
||||
})
|
||||
|
||||
expect(tooltipDirective.mounted).toHaveBeenCalled()
|
||||
const mountedCall = tooltipDirective.mounted.mock.calls[0]
|
||||
const binding = mountedCall[1]
|
||||
expect(binding.value).toBeDefined()
|
||||
})
|
||||
|
||||
it('uses tooltip container from provide/inject', () => {
|
||||
const { tooltipDirective } = renderHeader({
|
||||
nodeData: makeNodeData({ type: 'KSampler' })
|
||||
})
|
||||
|
||||
expect(tooltipDirective.mounted).toHaveBeenCalled()
|
||||
const mountedEl = tooltipDirective.mounted.mock.calls[0][0]
|
||||
expect(mountedEl).toBe(screen.getByTestId('node-title'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,21 +38,28 @@
|
||||
</Button>
|
||||
</div>
|
||||
<!-- Node Title -->
|
||||
<div
|
||||
v-tooltip.top="tooltipConfig"
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
data-testid="node-title"
|
||||
<BaseTooltip
|
||||
:text="isEditing ? '' : getNodeDescription"
|
||||
side="top"
|
||||
size="large"
|
||||
:delay-duration="tooltipDelay"
|
||||
:disabled="!tooltipsEnabled"
|
||||
>
|
||||
<div class="flex-1 truncate">
|
||||
<EditableText
|
||||
:model-value="displayTitle"
|
||||
:is-editing="isEditing"
|
||||
:input-attrs="{ 'data-testid': 'node-title-input' }"
|
||||
@edit="handleTitleEdit"
|
||||
@cancel="handleTitleCancel"
|
||||
/>
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center gap-2"
|
||||
data-testid="node-title"
|
||||
>
|
||||
<div class="flex-1 truncate">
|
||||
<EditableText
|
||||
:model-value="displayTitle"
|
||||
:is-editing="isEditing"
|
||||
:input-attrs="{ 'data-testid': 'node-title-input' }"
|
||||
@edit="handleTitleEdit"
|
||||
@cancel="handleTitleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseTooltip>
|
||||
</div>
|
||||
|
||||
<template v-for="badge in priceBadges ?? []" :key="badge.required">
|
||||
@@ -89,6 +96,7 @@ import { computed, onErrorCaptured, ref, watch } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
@@ -126,18 +134,10 @@ onErrorCaptured((error) => {
|
||||
// Editing state
|
||||
const isEditing = ref(false)
|
||||
|
||||
const { getNodeDescription, createTooltipConfig } = useNodeTooltips(
|
||||
const { getNodeDescription, tooltipsEnabled, tooltipDelay } = useNodeTooltips(
|
||||
nodeData?.type || ''
|
||||
)
|
||||
|
||||
const tooltipConfig = computed(() => {
|
||||
if (isEditing.value) {
|
||||
return { value: '', disabled: true }
|
||||
}
|
||||
const description = getNodeDescription.value
|
||||
return createTooltipConfig(description)
|
||||
})
|
||||
|
||||
const resolveTitle = (info: VueNodeData | undefined) => {
|
||||
const untitledLabel = st('g.untitled', 'Untitled')
|
||||
return resolveNodeDisplayName(info ?? null, {
|
||||
|
||||
@@ -86,7 +86,8 @@ describe('NodeWidgets', () => {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
stubs: {
|
||||
InputSlot: true
|
||||
InputSlot: true,
|
||||
BaseTooltip: { template: '<slot />' }
|
||||
},
|
||||
mocks: {
|
||||
$t: (key: string) => key
|
||||
|
||||
@@ -57,22 +57,29 @@
|
||||
:name="widget.name"
|
||||
:enable="canSelectInputs && !widget.simplified.options?.disabled"
|
||||
>
|
||||
<component
|
||||
:is="widget.vueComponent"
|
||||
v-model="widget.value"
|
||||
v-tooltip.left="widget.tooltipConfig"
|
||||
:widget="widget.simplified"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:node-type="nodeType"
|
||||
:class="
|
||||
cn(
|
||||
'col-span-2',
|
||||
widget.hasError && 'font-bold text-node-stroke-error'
|
||||
)
|
||||
"
|
||||
@update:model-value="widget.updateHandler"
|
||||
@contextmenu="widget.handleContextMenu"
|
||||
/>
|
||||
<BaseTooltip
|
||||
:text="widget.tooltipText"
|
||||
side="left"
|
||||
size="large"
|
||||
:delay-duration="tooltipDelay"
|
||||
:disabled="!tooltipsEnabled"
|
||||
>
|
||||
<component
|
||||
:is="widget.vueComponent"
|
||||
v-model="widget.value"
|
||||
:widget="widget.simplified"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:node-type="nodeType"
|
||||
:class="
|
||||
cn(
|
||||
'col-span-2',
|
||||
widget.hasError && 'font-bold text-node-stroke-error'
|
||||
)
|
||||
"
|
||||
@update:model-value="widget.updateHandler"
|
||||
@contextmenu="widget.handleContextMenu"
|
||||
/>
|
||||
</BaseTooltip>
|
||||
</AppInput>
|
||||
</div>
|
||||
</template>
|
||||
@@ -82,11 +89,13 @@
|
||||
<script setup lang="ts">
|
||||
import { onErrorCaptured, ref } from 'vue'
|
||||
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import AppInput from '@/renderer/extensions/linearMode/AppInput.vue'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import { useProcessedWidgets } from '@/renderer/extensions/vueNodes/composables/useProcessedWidgets'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -126,11 +135,7 @@ onErrorCaptured((error) => {
|
||||
return false
|
||||
})
|
||||
|
||||
const {
|
||||
canSelectInputs,
|
||||
gridTemplateRows,
|
||||
nodeType,
|
||||
processedWidgets,
|
||||
showAdvanced
|
||||
} = useProcessedWidgets(() => nodeData)
|
||||
const { nodeType, showAdvanced, canSelectInputs, processedWidgets, gridTemplateRows } =
|
||||
useProcessedWidgets(() => nodeData)
|
||||
const { tooltipsEnabled, tooltipDelay } = useNodeTooltips(nodeType)
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent } from 'vue'
|
||||
import { computed, defineComponent } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
@@ -22,7 +22,8 @@ vi.mock('@/renderer/core/canvas/links/slotLinkDragUIState', () => ({
|
||||
vi.mock('@/renderer/extensions/vueNodes/composables/useNodeTooltips', () => ({
|
||||
useNodeTooltips: () => ({
|
||||
getOutputSlotTooltip: () => '',
|
||||
createTooltipConfig: (text: string) => ({ value: text })
|
||||
tooltipsEnabled: computed(() => false),
|
||||
tooltipDelay: computed(() => 0)
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -53,6 +54,11 @@ const i18n = createI18n({
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
const BaseTooltipStub = defineComponent({
|
||||
name: 'BaseTooltip',
|
||||
template: '<slot />'
|
||||
})
|
||||
|
||||
function renderOutputSlot(slotData: Partial<INodeSlot>, index = 0) {
|
||||
return render(OutputSlot, {
|
||||
props: {
|
||||
@@ -62,8 +68,10 @@ function renderOutputSlot(slotData: Partial<INodeSlot>, index = 0) {
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: {} },
|
||||
stubs: { SlotConnectionDot: SlotConnectionDotStub }
|
||||
stubs: {
|
||||
SlotConnectionDot: SlotConnectionDotStub,
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
<template>
|
||||
<div v-if="renderError" class="node-error p-1 text-xs text-red-500">⚠️</div>
|
||||
<div v-else v-tooltip.right="tooltipConfig" :class="slotWrapperClass">
|
||||
<div class="relative flex h-full min-w-0 items-center">
|
||||
<!-- Slot Name -->
|
||||
<span
|
||||
v-if="!props.dotOnly && !hasNoLabel"
|
||||
class="truncate text-node-component-slot-text"
|
||||
>
|
||||
{{
|
||||
slotData.label ||
|
||||
slotData.localized_name ||
|
||||
(slotData.name ?? `Output ${index}`)
|
||||
}}
|
||||
</span>
|
||||
<BaseTooltip
|
||||
v-else
|
||||
:text="outputTooltipText"
|
||||
side="right"
|
||||
size="large"
|
||||
:delay-duration="tooltipDelay"
|
||||
:disabled="!tooltipsEnabled"
|
||||
>
|
||||
<div :class="slotWrapperClass">
|
||||
<div class="relative flex h-full min-w-0 items-center">
|
||||
<!-- Slot Name -->
|
||||
<span
|
||||
v-if="!props.dotOnly && !hasNoLabel"
|
||||
class="truncate text-node-component-slot-text"
|
||||
>
|
||||
{{
|
||||
slotData.label ||
|
||||
slotData.localized_name ||
|
||||
(slotData.name ?? `Output ${index}`)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Connection Dot -->
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
class="w-3 translate-x-1/2"
|
||||
:slot-data
|
||||
@pointerdown="onPointerDown"
|
||||
/>
|
||||
</div>
|
||||
<!-- Connection Dot -->
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
class="w-3 translate-x-1/2"
|
||||
:slot-data
|
||||
@pointerdown="onPointerDown"
|
||||
/>
|
||||
</div>
|
||||
</BaseTooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -29,6 +38,7 @@ import { computed, onErrorCaptured, ref, watchEffect } from 'vue'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||
import { RenderShape } from '@/lib/litegraph/src/types/globalEnums'
|
||||
@@ -65,11 +75,11 @@ const renderError = ref<string | null>(null)
|
||||
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
const { getOutputSlotTooltip, createTooltipConfig } = useNodeTooltips(
|
||||
const { getOutputSlotTooltip, tooltipsEnabled, tooltipDelay } = useNodeTooltips(
|
||||
props.nodeType || ''
|
||||
)
|
||||
|
||||
const tooltipConfig = computed(() => {
|
||||
const outputTooltipText = computed(() => {
|
||||
const slotName = props.slotData.name || ''
|
||||
const tooltipText = getOutputSlotTooltip(props.index)
|
||||
const fallbackText = tooltipText || `Output: ${slotName}`
|
||||
@@ -77,7 +87,7 @@ const tooltipConfig = computed(() => {
|
||||
props.slotData.shape === RenderShape.GRID
|
||||
? ` ${t('vueNodesSlot.iterative')}`
|
||||
: ''
|
||||
return createTooltipConfig(fallbackText + iterativeSuffix)
|
||||
return fallbackText + iterativeSuffix
|
||||
})
|
||||
|
||||
onErrorCaptured((error) => {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
TooltipOptions,
|
||||
TooltipPassThroughMethodOptions
|
||||
} from 'primevue/tooltip'
|
||||
import { computed, ref, unref } from 'vue'
|
||||
import { computed, unref } from 'vue'
|
||||
import type { MaybeRef } from 'vue'
|
||||
|
||||
import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
|
||||
@@ -10,77 +6,6 @@ import { st } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
// PrimeVue adds this internal property to elements with tooltips
|
||||
interface PrimeVueTooltipElement extends Element {
|
||||
$_ptooltipId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide all visible tooltips by dispatching mouseleave events
|
||||
*
|
||||
*
|
||||
* IMPORTANT: this escape is needed for many reason due to primevue's directive tooltip system.
|
||||
* We cannot use PT to conditionally render the tooltips because the entire PT object only run
|
||||
* once during the initialization of the directive not every mount/unmount.
|
||||
* Once the directive is constructed its no longer reactive in the traditional sense.
|
||||
* We have to use something non destructive like mouseevents to dismiss the tooltip.
|
||||
*
|
||||
* TODO: use a better tooltip component like RekaUI for vue nodes specifically.
|
||||
*/
|
||||
|
||||
const tooltipsTemporarilyDisabled = ref(false)
|
||||
|
||||
const hideTooltipsGlobally = () => {
|
||||
// Get all visible tooltip elements
|
||||
const tooltips = document.querySelectorAll('.p-tooltip')
|
||||
|
||||
// Early return if no tooltips are visible
|
||||
if (tooltips.length === 0) return
|
||||
|
||||
tooltips.forEach((tooltipEl) => {
|
||||
const tooltipId = tooltipEl.id
|
||||
if (!tooltipId) return
|
||||
|
||||
// Find the target element that owns this tooltip
|
||||
const targetElements = document.querySelectorAll('[data-pd-tooltip="true"]')
|
||||
for (const targetEl of targetElements) {
|
||||
if ((targetEl as PrimeVueTooltipElement).$_ptooltipId === tooltipId) {
|
||||
;(targetEl as HTMLElement).dispatchEvent(
|
||||
new MouseEvent('mouseleave', { bubbles: true })
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Disable tooltips temporarily after hiding (for drag operations)
|
||||
tooltipsTemporarilyDisabled.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-enable tooltips after pointer interaction ends
|
||||
*/
|
||||
const handlePointerUp = () => {
|
||||
tooltipsTemporarilyDisabled.value = false
|
||||
}
|
||||
|
||||
// Global tooltip hiding system
|
||||
const globalTooltipState = { listenersSetup: false }
|
||||
|
||||
function setupGlobalTooltipHiding() {
|
||||
if (globalTooltipState.listenersSetup) return
|
||||
|
||||
document.addEventListener('pointerdown', hideTooltipsGlobally)
|
||||
document.addEventListener('pointerup', handlePointerUp)
|
||||
window.addEventListener('wheel', hideTooltipsGlobally, {
|
||||
capture: true, //Need this to bypass the event layer from Litegraph
|
||||
passive: true
|
||||
})
|
||||
|
||||
globalTooltipState.listenersSetup = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing Vue node tooltips
|
||||
@@ -90,15 +15,14 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const settingsStore = useSettingStore()
|
||||
|
||||
// Setup global pointerdown listener once
|
||||
setupGlobalTooltipHiding()
|
||||
|
||||
// Check if tooltips are globally enabled
|
||||
const tooltipsEnabled = computed(() =>
|
||||
settingsStore.get('Comfy.EnableTooltips')
|
||||
)
|
||||
|
||||
// Get node definition for tooltip data
|
||||
const tooltipDelay = computed(
|
||||
() => settingsStore.get('LiteGraph.Node.TooltipDelay') as number
|
||||
)
|
||||
|
||||
const nodeDef = computed(() => nodeDefStore.nodeDefsByName[unref(nodeType)])
|
||||
|
||||
/**
|
||||
@@ -114,7 +38,7 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
/**
|
||||
* Get tooltip text for input slots
|
||||
*/
|
||||
const getInputSlotTooltip = (slotName: string) => {
|
||||
function getInputSlotTooltip(slotName: string) {
|
||||
if (!tooltipsEnabled.value || !nodeDef.value) return ''
|
||||
|
||||
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(slotName)}.tooltip`
|
||||
@@ -125,7 +49,7 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
/**
|
||||
* Get tooltip text for output slots
|
||||
*/
|
||||
const getOutputSlotTooltip = (slotIndex: number) => {
|
||||
function getOutputSlotTooltip(slotIndex: number) {
|
||||
if (!tooltipsEnabled.value || !nodeDef.value) return ''
|
||||
|
||||
const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.outputs.${slotIndex}.tooltip`
|
||||
@@ -136,7 +60,7 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
/**
|
||||
* Get tooltip text for widgets
|
||||
*/
|
||||
const getWidgetTooltip = (widget: SafeWidgetData) => {
|
||||
function getWidgetTooltip(widget: SafeWidgetData) {
|
||||
if (!tooltipsEnabled.value || !nodeDef.value) return ''
|
||||
|
||||
// First try widget-specific tooltip
|
||||
@@ -149,46 +73,12 @@ export function useNodeTooltips(nodeType: MaybeRef<string>) {
|
||||
return st(key, inputTooltip)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tooltip configuration object for v-tooltip directive
|
||||
* Components wrap this in computed() for reactivity
|
||||
*/
|
||||
const createTooltipConfig = (text: string): TooltipOptions => {
|
||||
const tooltipDelay = settingsStore.get('LiteGraph.Node.TooltipDelay')
|
||||
const tooltipText = text || ''
|
||||
|
||||
return {
|
||||
value: tooltipText,
|
||||
showDelay: tooltipDelay as number,
|
||||
hideDelay: 0, // Immediate hiding
|
||||
disabled:
|
||||
!tooltipsEnabled.value ||
|
||||
!tooltipText ||
|
||||
tooltipsTemporarilyDisabled.value, // this reactive value works but only on next mount,
|
||||
// so if the tooltip is already visible changing this will not hide it
|
||||
pt: {
|
||||
text: {
|
||||
class:
|
||||
'border-node-component-tooltip-border bg-node-component-tooltip-surface border rounded-md px-4 py-2 text-node-component-tooltip text-sm font-normal leading-tight max-w-75 shadow-none'
|
||||
},
|
||||
arrow: ({ context }: TooltipPassThroughMethodOptions) => ({
|
||||
class: cn(
|
||||
context.top && 'border-t-node-component-tooltip-border',
|
||||
context.bottom && 'border-b-node-component-tooltip-border',
|
||||
context.left && 'border-l-node-component-tooltip-border',
|
||||
context.right && 'border-r-node-component-tooltip-border'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tooltipsEnabled,
|
||||
tooltipDelay,
|
||||
getNodeDescription,
|
||||
getInputSlotTooltip,
|
||||
getOutputSlotTooltip,
|
||||
getWidgetTooltip,
|
||||
createTooltipConfig
|
||||
getWidgetTooltip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { TooltipOptions } from 'primevue'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -198,7 +197,7 @@ describe('hasWidgetError', () => {
|
||||
})
|
||||
|
||||
const noopUi = {
|
||||
getTooltipConfig: () => ({}) as TooltipOptions,
|
||||
getTooltipText: () => '',
|
||||
handleNodeRightClick: () => {}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { TooltipOptions } from 'primevue'
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
@@ -53,7 +52,7 @@ interface ProcessedWidget {
|
||||
name: string
|
||||
renderKey: string
|
||||
simplified: SimplifiedWidget
|
||||
tooltipConfig: TooltipOptions
|
||||
tooltipText: string
|
||||
type: string
|
||||
updateHandler: (value: WidgetValue) => void
|
||||
value: WidgetValue
|
||||
@@ -62,7 +61,7 @@ interface ProcessedWidget {
|
||||
}
|
||||
|
||||
interface WidgetUiCallbacks {
|
||||
getTooltipConfig: (widget: SafeWidgetData) => TooltipOptions
|
||||
getTooltipText: (widget: SafeWidgetData) => string
|
||||
handleNodeRightClick: (e: PointerEvent, nodeId: string) => void
|
||||
}
|
||||
|
||||
@@ -318,7 +317,7 @@ export function computeProcessedWidgets({
|
||||
executionErrorStore
|
||||
)
|
||||
|
||||
const tooltipConfig = ui.getTooltipConfig(widget)
|
||||
const tooltipText = ui.getTooltipText(widget)
|
||||
const handleContextMenu = (e: PointerEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -352,7 +351,7 @@ export function computeProcessedWidgets({
|
||||
simplified,
|
||||
value,
|
||||
updateHandler,
|
||||
tooltipConfig,
|
||||
tooltipText,
|
||||
slotMetadata
|
||||
})
|
||||
}
|
||||
@@ -369,10 +368,10 @@ export function useProcessedWidgets(
|
||||
const { handleNodeRightClick } = useNodeEventHandlers()
|
||||
|
||||
const nodeType = computed(() => nodeDataGetter()?.type || '')
|
||||
const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(nodeType)
|
||||
const { getWidgetTooltip } = useNodeTooltips(nodeType)
|
||||
|
||||
const ui: WidgetUiCallbacks = {
|
||||
getTooltipConfig: (widget) => createTooltipConfig(getWidgetTooltip(widget)),
|
||||
getTooltipText: (widget) => getWidgetTooltip(widget),
|
||||
handleNodeRightClick
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user