mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-13 03:01:33 +00:00
Compare commits
11 Commits
refactor/n
...
feat/toolt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e53b997c | ||
|
|
3aef8da214 | ||
|
|
5b2f4b303a | ||
|
|
7bf2120cc5 | ||
|
|
8cd39fc24f | ||
|
|
1065c3da23 | ||
|
|
30e4b443d1 | ||
|
|
77105602c0 | ||
|
|
5e8146267a | ||
|
|
f8e2a81666 | ||
|
|
d429a10ea6 |
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 '../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'
|
||||
|
||||
@@ -119,6 +119,7 @@ function createWrapper({
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<slot />' },
|
||||
SubgraphBreadcrumb: true,
|
||||
QueueProgressOverlay: true,
|
||||
QueueInlineProgressSummary: true,
|
||||
@@ -131,9 +132,6 @@ function createWrapper({
|
||||
template: '<div />'
|
||||
},
|
||||
...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(() => {
|
||||
|
||||
@@ -52,11 +52,17 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
|
||||
useSidebarTabStore: () => mockSidebarTabStore
|
||||
}))
|
||||
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
const mountMenu = () =>
|
||||
mount(JobHistoryActionsMenu, {
|
||||
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)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import QueueOverlayActive from './QueueOverlayActive.vue'
|
||||
import * as tooltipConfig from '@/composables/useTooltipConfig'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -27,11 +26,6 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
const tooltipDirectiveStub = {
|
||||
mounted: vi.fn(),
|
||||
updated: vi.fn()
|
||||
}
|
||||
|
||||
const SELECTORS = {
|
||||
interruptAllButton: 'button[aria-label="Interrupt all running jobs"]',
|
||||
clearQueuedButton: 'button[aria-label="Clear queued"]',
|
||||
@@ -43,6 +37,10 @@ const COPY = {
|
||||
viewAllJobs: 'View all jobs'
|
||||
}
|
||||
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
const mountComponent = (props: Record<string, unknown> = {}) =>
|
||||
mount(QueueOverlayActive, {
|
||||
props: {
|
||||
@@ -58,8 +56,8 @@ const mountComponent = (props: Record<string, unknown> = {}) =>
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: {
|
||||
tooltip: tooltipDirectiveStub
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -113,13 +111,4 @@ describe('QueueOverlayActive', () => {
|
||||
expect(wrapper.find(SELECTORS.interruptAllButton).exists()).toBe(false)
|
||||
expect(wrapper.find(SELECTORS.clearQueuedButton).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('builds tooltip configs with translated strings', () => {
|
||||
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
|
||||
|
||||
mountComponent()
|
||||
|
||||
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>
|
||||
|
||||
@@ -48,11 +48,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 mountHeader = (props = {}) =>
|
||||
@@ -64,7 +62,9 @@ const mountHeader = (props = {}) =>
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: tooltipDirectiveStub }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -105,14 +105,11 @@ describe('QueueOverlayHeader', () => {
|
||||
})
|
||||
|
||||
it('emits clear history from the menu', async () => {
|
||||
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
|
||||
|
||||
const wrapper = mountHeader()
|
||||
|
||||
expect(wrapper.find('button[aria-label="More options"]').exists()).toBe(
|
||||
true
|
||||
)
|
||||
expect(spy).toHaveBeenCalledWith('More')
|
||||
|
||||
const clearHistoryButton = wrapper.get(
|
||||
'[data-testid="clear-history-action"]'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -54,6 +54,10 @@ const i18n = createI18n({
|
||||
}
|
||||
})
|
||||
|
||||
const BaseTooltipStub = {
|
||||
template: '<slot />'
|
||||
}
|
||||
|
||||
describe('JobFiltersBar', () => {
|
||||
it('emits showAssets when the assets icon button is clicked', async () => {
|
||||
const wrapper = mount(JobFiltersBar, {
|
||||
@@ -65,7 +69,9 @@ describe('JobFiltersBar', () => {
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: { tooltip: () => undefined }
|
||||
stubs: {
|
||||
BaseTooltip: BaseTooltipStub
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -88,7 +94,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[]
|
||||
|
||||
@@ -53,7 +53,10 @@ describe(ShareAssetWarningBox, () => {
|
||||
...props
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
BaseTooltip: { template: '<slot />' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -187,6 +187,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>',
|
||||
|
||||
Reference in New Issue
Block a user