mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-07 22:44:49 +00:00
Compare commits
3 Commits
main
...
codex/fe-5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4d8df259 | ||
|
|
c3fe2a5939 | ||
|
|
82e3068181 |
575
browser_tests/tests/primevueRekaOverlayZIndex.spec.ts
Normal file
575
browser_tests/tests/primevueRekaOverlayZIndex.spec.ts
Normal file
@@ -0,0 +1,575 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { AlgoliaNodePack } from '@/types/algoliaTypes'
|
||||
import type {
|
||||
AssetMetadata,
|
||||
AssetItem,
|
||||
ModelFolder
|
||||
} from '@/platform/assets/schemas/assetSchema'
|
||||
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||
import type { components as RegistryComponents } from '@comfyorg/registry-types'
|
||||
import {
|
||||
STABLE_CHECKPOINT,
|
||||
STABLE_LORA,
|
||||
STABLE_VAE
|
||||
} from '@e2e/fixtures/data/assetFixtures'
|
||||
import {
|
||||
makeTemplate,
|
||||
mockTemplateIndex
|
||||
} from '@e2e/fixtures/data/templateFixtures'
|
||||
import { mockSystemStats } from '@e2e/fixtures/data/systemStats'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
type InstalledPacksResponse =
|
||||
ManagerComponents['schemas']['InstalledPacksResponse']
|
||||
type RegistryNodePack = RegistryComponents['schemas']['Node']
|
||||
|
||||
interface AlgoliaSearchResult {
|
||||
hits: Array<Partial<AlgoliaNodePack> | AlgoliaSuggestionHit>
|
||||
nbHits: number
|
||||
page: number
|
||||
nbPages: number
|
||||
hitsPerPage: number
|
||||
}
|
||||
|
||||
interface AlgoliaSearchResponse {
|
||||
results: AlgoliaSearchResult[]
|
||||
}
|
||||
|
||||
interface AlgoliaSuggestionHit {
|
||||
query: string
|
||||
popularity: number
|
||||
}
|
||||
|
||||
const RAISED_DIALOG_Z_INDEX = 4200
|
||||
|
||||
const MODEL_ASSETS: AssetItem[] = [
|
||||
{
|
||||
...STABLE_CHECKPOINT,
|
||||
id: 'fe-569-checkpoint',
|
||||
tags: STABLE_CHECKPOINT.tags ?? [],
|
||||
is_immutable: false
|
||||
},
|
||||
{
|
||||
...STABLE_LORA,
|
||||
id: 'fe-569-lora',
|
||||
tags: STABLE_LORA.tags ?? [],
|
||||
is_immutable: false
|
||||
},
|
||||
{
|
||||
...STABLE_VAE,
|
||||
id: 'fe-569-vae',
|
||||
tags: STABLE_VAE.tags ?? [],
|
||||
is_immutable: false
|
||||
}
|
||||
]
|
||||
|
||||
const MODEL_FOLDERS: ModelFolder[] = [
|
||||
{ name: 'checkpoints', folders: [] },
|
||||
{ name: 'loras', folders: [] },
|
||||
{ name: 'vae', folders: [] }
|
||||
]
|
||||
|
||||
const MOCK_PACK_A: RegistryNodePack = {
|
||||
id: 'test-pack-a',
|
||||
name: 'Test Pack A',
|
||||
description: 'A test custom node pack',
|
||||
downloads: 5000,
|
||||
status: 'NodeStatusActive',
|
||||
publisher: { id: 'test-publisher', name: 'Test Publisher' },
|
||||
latest_version: { version: '1.0.0', status: 'NodeVersionStatusActive' },
|
||||
repository: 'https://github.com/test/pack-a',
|
||||
tags: ['image', 'processing']
|
||||
}
|
||||
|
||||
const MOCK_PACK_B: RegistryNodePack = {
|
||||
id: 'test-pack-b',
|
||||
name: 'Test Pack B',
|
||||
description: 'Another test custom node pack for testing search',
|
||||
downloads: 3000,
|
||||
status: 'NodeStatusActive',
|
||||
publisher: { id: 'another-publisher', name: 'Another Publisher' },
|
||||
latest_version: { version: '2.1.0', status: 'NodeVersionStatusActive' },
|
||||
repository: 'https://github.com/test/pack-b',
|
||||
tags: ['video', 'generation']
|
||||
}
|
||||
|
||||
const MOCK_INSTALLED_PACKS: InstalledPacksResponse = {
|
||||
'test-pack-a': {
|
||||
ver: '1.0.0',
|
||||
cnr_id: 'test-pack-a',
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
const MOCK_HIT_A: Partial<AlgoliaNodePack> = {
|
||||
objectID: 'test-pack-a',
|
||||
id: 'test-pack-a',
|
||||
name: 'Test Pack A',
|
||||
description: 'A test custom node pack',
|
||||
total_install: 5000,
|
||||
status: 'NodeStatusActive',
|
||||
publisher_id: 'test-publisher',
|
||||
latest_version: '1.0.0',
|
||||
latest_version_status: 'NodeVersionStatusActive',
|
||||
repository_url: 'https://github.com/test/pack-a',
|
||||
comfy_nodes: ['TestNodeA'],
|
||||
create_time: '2024-01-01T00:00:00Z',
|
||||
update_time: '2024-06-01T00:00:00Z',
|
||||
license: 'MIT',
|
||||
tags: ['image', 'processing']
|
||||
}
|
||||
|
||||
const MOCK_HIT_B: Partial<AlgoliaNodePack> = {
|
||||
objectID: 'test-pack-b',
|
||||
id: 'test-pack-b',
|
||||
name: 'Test Pack B',
|
||||
description: 'Another test custom node pack',
|
||||
total_install: 3000,
|
||||
status: 'NodeStatusActive',
|
||||
publisher_id: 'another-publisher',
|
||||
latest_version: '2.1.0',
|
||||
latest_version_status: 'NodeVersionStatusActive',
|
||||
repository_url: 'https://github.com/test/pack-b',
|
||||
comfy_nodes: ['TestNodeB'],
|
||||
create_time: '2024-02-01T00:00:00Z',
|
||||
update_time: '2024-07-01T00:00:00Z',
|
||||
license: 'Apache-2.0',
|
||||
tags: ['video', 'generation']
|
||||
}
|
||||
|
||||
const MOCK_ALGOLIA_RESPONSE: AlgoliaSearchResponse = {
|
||||
results: [
|
||||
{
|
||||
hits: [MOCK_HIT_A, MOCK_HIT_B],
|
||||
nbHits: 2,
|
||||
page: 0,
|
||||
nbPages: 1,
|
||||
hitsPerPage: 20
|
||||
},
|
||||
{
|
||||
hits: [{ query: 'Test Pack A', popularity: 100 }],
|
||||
nbHits: 1,
|
||||
page: 0,
|
||||
nbPages: 1,
|
||||
hitsPerPage: 20
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
test.use({
|
||||
initialFeatureFlags: {
|
||||
model_upload_button_enabled: true,
|
||||
private_models_enabled: true
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('PrimeVue dialog child overlays', () => {
|
||||
test('keeps workflow template filters above the template dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await mockTemplateLibrary(comfyPage)
|
||||
await forcePrimeVueDialogZIndex(comfyPage.page)
|
||||
|
||||
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
|
||||
const dialog = comfyPage.templatesDialog.root
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
await dialog.getByRole('button', { name: /Model Filter/ }).click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'Flux' })
|
||||
)
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
await dialog.getByRole('combobox', { name: /Sort by/ }).click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'Popular' })
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps manager header controls above the manager dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await setupManagerDialog(comfyPage)
|
||||
await forcePrimeVueDialogZIndex(comfyPage.page)
|
||||
await comfyPage.command.executeCommand('Comfy.OpenManagerDialog')
|
||||
|
||||
const dialog = comfyPage.page.getByRole('dialog').last()
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
await dialog.getByText('Node Pack').first().click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'Nodes' })
|
||||
)
|
||||
await comfyPage.page.getByRole('option', { name: 'Nodes' }).click()
|
||||
|
||||
await dialog.getByRole('combobox', { name: 'Sort' }).click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'Name' })
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps asset browser filters above the asset browser dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const dialog = await openAssetBrowser(comfyPage)
|
||||
|
||||
await dialog.getByRole('button', { name: 'File formats' }).click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: '.safetensors' })
|
||||
)
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
await dialog.getByRole('combobox', { name: 'Sort by' }).click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'A-Z' })
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps the model info selector above the asset browser dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const dialog = await openAssetBrowser(comfyPage)
|
||||
const card = dialog
|
||||
.locator('[data-component-id="AssetCard"]')
|
||||
.filter({ hasText: STABLE_CHECKPOINT.name })
|
||||
.first()
|
||||
|
||||
await card.hover()
|
||||
await card.getByRole('button', { name: 'Model Info' }).click()
|
||||
|
||||
const modelInfoPanel = dialog.locator(
|
||||
'[data-component-id="ModelInfoPanel"]'
|
||||
)
|
||||
await expect(modelInfoPanel).toBeVisible()
|
||||
|
||||
await modelInfoPanel.getByRole('combobox').click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'LoRA' })
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps the upload model selector above the upload dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const assetBrowserDialog = await openAssetBrowser(comfyPage)
|
||||
await mockRemoteModelMetadata(comfyPage.page)
|
||||
|
||||
await assetBrowserDialog
|
||||
.locator('[data-attr="upload-model-button"]')
|
||||
.click()
|
||||
|
||||
const uploadDialog = comfyPage.page.getByRole('dialog').last()
|
||||
await expect(uploadDialog).toContainText('Import a model')
|
||||
|
||||
await uploadDialog
|
||||
.locator('[data-attr="upload-model-step1-url-input"]')
|
||||
.fill('https://civitai.com/models/123/fe-569-test')
|
||||
await uploadDialog
|
||||
.locator('[data-attr="upload-model-step1-continue-button"]')
|
||||
.click()
|
||||
|
||||
const trigger = uploadDialog.locator(
|
||||
'[data-attr="upload-model-step2-type-selector"]'
|
||||
)
|
||||
await expect(trigger).toBeVisible()
|
||||
await trigger.click()
|
||||
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'LoRA' })
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps keybinding controls above the settings dialog', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await registerNoBindingCommand(comfyPage)
|
||||
await forcePrimeVueDialogZIndex(comfyPage.page)
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.settingDialog.category('Keybinding').click()
|
||||
|
||||
const dialog = comfyPage.settingDialog.root
|
||||
await expect(
|
||||
comfyPage.page.getByPlaceholder('Search Keybindings...')
|
||||
).toBeVisible()
|
||||
|
||||
await dialog
|
||||
.locator('#keybinding-panel-actions')
|
||||
.locator('button[role="combobox"]')
|
||||
.click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('option', { name: 'Default Preset' })
|
||||
)
|
||||
await comfyPage.page.getByRole('option', { name: 'Default Preset' }).click()
|
||||
|
||||
await searchKeybindings(comfyPage.page, 'Comfy.SaveWorkflow')
|
||||
const saveWorkflowCommand = /^Comfy\.SaveWorkflow$/
|
||||
await getCommandRow(comfyPage.page, saveWorkflowCommand)
|
||||
.getByTitle(saveWorkflowCommand)
|
||||
.click({ button: 'right' })
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('menuitem', { name: /Change keybinding/i })
|
||||
)
|
||||
|
||||
await comfyPage.page.getByTestId(TestIds.keybindings.presetMenu).click()
|
||||
await expectOverlayItemAboveRaisedDialog(
|
||||
comfyPage.page.getByRole('menuitem', { name: 'Import preset' })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
async function forcePrimeVueDialogZIndex(page: Page) {
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
.p-dialog-mask {
|
||||
z-index: ${RAISED_DIALOG_Z_INDEX} !important;
|
||||
}
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
async function expectOverlayItemAboveRaisedDialog(item: Locator) {
|
||||
await expect(item).toBeVisible()
|
||||
|
||||
const overlay = item.locator(
|
||||
'xpath=ancestor-or-self::*[contains(@style, "z-index")][1]'
|
||||
)
|
||||
await expect(overlay).toHaveCount(1)
|
||||
|
||||
const overlayZIndex = await readZIndex(overlay)
|
||||
|
||||
expect(overlayZIndex).toBeGreaterThan(RAISED_DIALOG_Z_INDEX)
|
||||
}
|
||||
|
||||
async function readZIndex(locator: Locator): Promise<number> {
|
||||
const rawZIndex = await locator.evaluate(
|
||||
(element) => getComputedStyle(element).zIndex
|
||||
)
|
||||
const zIndex = Number.parseInt(rawZIndex, 10)
|
||||
|
||||
expect(Number.isFinite(zIndex)).toBe(true)
|
||||
|
||||
return zIndex
|
||||
}
|
||||
|
||||
async function mockTemplateLibrary(comfyPage: ComfyPage) {
|
||||
await comfyPage.settings.setSetting('Comfy.Templates.SelectedModels', [])
|
||||
await comfyPage.settings.setSetting('Comfy.Templates.SelectedUseCases', [])
|
||||
await comfyPage.settings.setSetting('Comfy.Templates.SelectedRunsOn', [])
|
||||
await comfyPage.settings.setSetting('Comfy.Templates.SortBy', 'default')
|
||||
|
||||
await comfyPage.page.route('**/templates/**.webp', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
path: 'browser_tests/assets/example.webp',
|
||||
headers: {
|
||||
'Content-Type': 'image/webp',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.page.route('**/templates/index.json', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(
|
||||
mockTemplateIndex([
|
||||
makeTemplate({
|
||||
name: 'flux-template',
|
||||
title: 'Flux Template',
|
||||
models: ['Flux'],
|
||||
tags: ['Image']
|
||||
}),
|
||||
makeTemplate({
|
||||
name: 'wan-template',
|
||||
title: 'Wan Template',
|
||||
models: ['Wan 2.2'],
|
||||
tags: ['Video'],
|
||||
openSource: false
|
||||
})
|
||||
])
|
||||
),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function setupManagerDialog(comfyPage: ComfyPage) {
|
||||
const statsWithManager = {
|
||||
...mockSystemStats,
|
||||
system: {
|
||||
...mockSystemStats.system,
|
||||
argv: ['main.py', '--enable-manager']
|
||||
}
|
||||
}
|
||||
|
||||
await comfyPage.page.route('**/system_stats**', async (route) => {
|
||||
await route.fulfill({ json: statsWithManager })
|
||||
})
|
||||
|
||||
await comfyPage.page.route('**/v2/customnode/installed**', async (route) => {
|
||||
await route.fulfill({ json: MOCK_INSTALLED_PACKS })
|
||||
})
|
||||
|
||||
await comfyPage.page.route('**/v2/manager/queue/status**', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await comfyPage.page.route('**/v2/manager/queue/history**', async (route) => {
|
||||
await route.fulfill({ json: {} })
|
||||
})
|
||||
|
||||
await comfyPage.page.route('**/*.algolia.net/**', async (route) => {
|
||||
await route.fulfill({ json: MOCK_ALGOLIA_RESPONSE })
|
||||
})
|
||||
await comfyPage.page.route('**/*.algolianet.com/**', async (route) => {
|
||||
await route.fulfill({ json: MOCK_ALGOLIA_RESPONSE })
|
||||
})
|
||||
|
||||
const registryListResponse = {
|
||||
total: 2,
|
||||
nodes: [MOCK_PACK_A, MOCK_PACK_B],
|
||||
page: 1,
|
||||
limit: 64,
|
||||
totalPages: 1
|
||||
}
|
||||
|
||||
await comfyPage.page.route(
|
||||
'**/api.comfy.org/nodes/search**',
|
||||
async (route) => {
|
||||
await route.fulfill({ json: registryListResponse })
|
||||
}
|
||||
)
|
||||
|
||||
await comfyPage.page.route(
|
||||
(url) => url.hostname === 'api.comfy.org' && url.pathname === '/nodes',
|
||||
async (route) => {
|
||||
await route.fulfill({ json: registryListResponse })
|
||||
}
|
||||
)
|
||||
|
||||
await comfyPage.page.route(
|
||||
'**/v2/customnode/getmappings**',
|
||||
async (route) => {
|
||||
await route.fulfill({ json: {} })
|
||||
}
|
||||
)
|
||||
|
||||
await comfyPage.page.route(
|
||||
'**/v2/customnode/import_fail_info**',
|
||||
async (route) => {
|
||||
await route.fulfill({ json: {} })
|
||||
}
|
||||
)
|
||||
|
||||
await comfyPage.setup()
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const api = window.app!.api
|
||||
api.serverFeatureFlags.value = {
|
||||
...api.serverFeatureFlags.value,
|
||||
extension: {
|
||||
manager: {
|
||||
supports_v4: true,
|
||||
supports_csrf_post: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function openAssetBrowser(comfyPage: ComfyPage): Promise<Locator> {
|
||||
await mockModelFolders(comfyPage.page)
|
||||
await forcePrimeVueDialogZIndex(comfyPage.page)
|
||||
await showAssetBrowserModal(comfyPage.page)
|
||||
|
||||
const assetBrowser = comfyPage.page.locator(
|
||||
'[data-component-id="AssetBrowserModal"]'
|
||||
)
|
||||
await expect(assetBrowser).toBeVisible()
|
||||
await expect(
|
||||
assetBrowser.getByRole('heading', { name: STABLE_CHECKPOINT.name })
|
||||
).toBeVisible()
|
||||
|
||||
return assetBrowser.locator('xpath=ancestor::*[@role="dialog"][1]')
|
||||
}
|
||||
|
||||
async function showAssetBrowserModal(page: Page) {
|
||||
await page.evaluate((assets) => {
|
||||
Object.assign(window, { __fe569ModelAssets: assets })
|
||||
}, MODEL_ASSETS)
|
||||
|
||||
await page.addScriptTag({
|
||||
type: 'module',
|
||||
content: `
|
||||
const { useDialogService } = await import('/src/services/dialogService.ts')
|
||||
const AssetBrowserModal = (await import('/src/platform/assets/components/AssetBrowserModal.vue')).default
|
||||
|
||||
useDialogService().showLayoutDialog({
|
||||
key: 'fe-569-asset-browser',
|
||||
component: AssetBrowserModal,
|
||||
props: {
|
||||
showLeftPanel: true,
|
||||
assetType: 'models',
|
||||
title: 'Model Library',
|
||||
overrideAssets: window.__fe569ModelAssets,
|
||||
onClose: () => {}
|
||||
}
|
||||
})
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
async function mockModelFolders(page: Page) {
|
||||
await page.route('**/experiment/models', async (route) => {
|
||||
await route.fulfill({ json: MODEL_FOLDERS })
|
||||
})
|
||||
}
|
||||
|
||||
async function mockRemoteModelMetadata(page: Page) {
|
||||
const metadata: AssetMetadata = {
|
||||
content_length: 1234,
|
||||
final_url: 'https://civitai.com/api/download/models/123',
|
||||
filename: 'fe-569-upload-test.safetensors',
|
||||
tags: []
|
||||
}
|
||||
|
||||
await page.route('**/assets/remote-metadata**', async (route) => {
|
||||
await route.fulfill({ json: metadata })
|
||||
})
|
||||
}
|
||||
|
||||
async function registerNoBindingCommand(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const app = window.app!
|
||||
app.registerExtension({
|
||||
name: 'TestExtension.PrimeVueRekaOverlayZIndex',
|
||||
commands: [
|
||||
{ id: 'TestCommand.PrimeVueRekaOverlay.NoBinding', function: () => {} }
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function searchKeybindings(page: Page, query: string) {
|
||||
await page.getByPlaceholder('Search Keybindings...').fill(query)
|
||||
}
|
||||
|
||||
function getCommandRow(page: Page, commandTitle: RegExp): Locator {
|
||||
return page
|
||||
.locator('.keybinding-panel tr')
|
||||
.filter({ has: page.getByTitle(commandTitle) })
|
||||
}
|
||||
@@ -40,7 +40,10 @@
|
||||
|
||||
<template #contentFilter>
|
||||
<div class="relative flex flex-wrap justify-between gap-2 px-6 pb-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<!-- Model Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedModelObjects"
|
||||
@@ -48,6 +51,7 @@
|
||||
class="w-[250px]"
|
||||
:label="modelFilterLabel"
|
||||
:options="modelOptions"
|
||||
:content-style="selectContentStyle"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
@@ -62,6 +66,7 @@
|
||||
v-model="selectedUseCaseObjects"
|
||||
:label="useCaseFilterLabel"
|
||||
:options="useCaseOptions"
|
||||
:content-style="selectContentStyle"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
@@ -76,6 +81,7 @@
|
||||
v-model="selectedRunsOnObjects"
|
||||
:label="runsOnFilterLabel"
|
||||
:options="runsOnOptions"
|
||||
:content-style="selectContentStyle"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
@@ -92,6 +98,7 @@
|
||||
v-model="sortBy"
|
||||
:label="$t('templateWorkflows.sorting', 'Sort by')"
|
||||
:options="sortOptions"
|
||||
:content-style="selectContentStyle"
|
||||
class="w-62.5"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -416,6 +423,7 @@ import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
import { useLazyPagination } from '@/composables/useLazyPagination'
|
||||
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
||||
import { useTemplateFiltering } from '@/composables/useTemplateFiltering'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
@@ -632,6 +640,8 @@ const selectedRunsOnObjects = computed({
|
||||
const loadingTemplate = ref<string | null>(null)
|
||||
const hoveredTemplate = ref<string | null>(null)
|
||||
const cardRefs = ref<HTMLElement[]>([])
|
||||
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
||||
const selectContentStyle = primeVueOverlay.contentStyle
|
||||
|
||||
// Force re-render key for templates when sorting changes
|
||||
const templateListKey = ref(0)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="keybinding-panel flex flex-col gap-2">
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="keybinding-panel flex flex-col gap-2"
|
||||
>
|
||||
<Teleport defer to="#keybinding-panel-header">
|
||||
<SearchInput
|
||||
v-model="filters['global'].value"
|
||||
@@ -15,10 +18,12 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<KeybindingPresetToolbar
|
||||
:preset-names="presetNames"
|
||||
:content-style="keybindingOverlayContentStyle"
|
||||
@presets-changed="refreshPresetList"
|
||||
/>
|
||||
<DropdownMenu
|
||||
:entries="menuEntries"
|
||||
:style="keybindingOverlayContentStyle"
|
||||
icon="icon-[lucide--ellipsis]"
|
||||
item-class="text-sm gap-2"
|
||||
button-size="unset"
|
||||
@@ -238,6 +243,7 @@
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuContent
|
||||
:style="keybindingOverlayContentStyle"
|
||||
class="z-1200 min-w-56 rounded-lg border border-border-subtle bg-base-background px-2 py-3 shadow-interface"
|
||||
>
|
||||
<ContextMenuItem
|
||||
@@ -314,6 +320,7 @@ import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import SearchInput from '@/components/ui/search-input/SearchInput.vue'
|
||||
import { useEditKeybindingDialog } from '@/composables/useEditKeybindingDialog'
|
||||
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
||||
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
@@ -337,6 +344,8 @@ const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const { t } = useI18n()
|
||||
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
||||
const keybindingOverlayContentStyle = primeVueOverlay.contentStyle
|
||||
|
||||
const presetNames = ref<string[]>([])
|
||||
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
{{ displayLabel }}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent class="max-w-64 min-w-0 **:[[role=listbox]]:gap-1">
|
||||
<SelectContent
|
||||
:style="contentStyle"
|
||||
class="max-w-64 min-w-0 **:[[role=listbox]]:gap-1"
|
||||
>
|
||||
<div class="max-w-60">
|
||||
<SelectItem
|
||||
value="default"
|
||||
@@ -46,6 +49,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -57,8 +61,9 @@ import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import { useKeybindingPresetService } from '@/platform/keybindings/presetService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
|
||||
const { presetNames } = defineProps<{
|
||||
const { presetNames, contentStyle } = defineProps<{
|
||||
presetNames: string[]
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
position="popper"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
:style="popoverStyle"
|
||||
:style="[popoverStyle, contentStyle]"
|
||||
:class="selectContentClass"
|
||||
@keydown="onContentKeydown"
|
||||
@focus-outside="preventFocusDismiss"
|
||||
@@ -152,6 +152,7 @@ import {
|
||||
ComboboxViewport
|
||||
} from 'reka-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -183,7 +184,8 @@ const {
|
||||
searchPlaceholder,
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth
|
||||
popoverMaxWidth,
|
||||
contentStyle
|
||||
} = defineProps<{
|
||||
/** Input label shown on the trigger button */
|
||||
label?: string
|
||||
@@ -207,6 +209,7 @@ const {
|
||||
popoverMinWidth?: string
|
||||
/** Maximum width of the popover (default: auto) */
|
||||
popoverMaxWidth?: string
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const selectedItems = defineModel<SelectOption[]>({
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
v-if="suggestions.length > 0"
|
||||
position="popper"
|
||||
:side-offset="4"
|
||||
:style="contentStyle"
|
||||
:class="
|
||||
cn(
|
||||
'z-3000 max-h-60 w-(--reka-combobox-trigger-width) overflow-y-auto',
|
||||
@@ -99,7 +100,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { HTMLAttributes, StyleValue } from 'vue'
|
||||
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import {
|
||||
@@ -132,7 +133,8 @@ const {
|
||||
suggestions = [],
|
||||
optionLabel,
|
||||
optionKey,
|
||||
class: className
|
||||
class: className,
|
||||
contentStyle
|
||||
} = defineProps<{
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
@@ -144,6 +146,7 @@ const {
|
||||
optionLabel?: keyof T & string
|
||||
optionKey?: keyof T & string
|
||||
class?: HTMLAttributes['class']
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
position="popper"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
:style="optionStyle"
|
||||
:style="[optionStyle, contentStyle]"
|
||||
:class="cn(selectContentClass, 'min-w-(--reka-select-trigger-width)')"
|
||||
@keydown="onContentKeydown"
|
||||
>
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
SelectViewport
|
||||
} from 'reka-ui'
|
||||
import { ref } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
@@ -108,7 +109,8 @@ const {
|
||||
disabled = false,
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth
|
||||
popoverMaxWidth,
|
||||
contentStyle
|
||||
} = defineProps<{
|
||||
label?: string
|
||||
options?: SelectOption[]
|
||||
@@ -126,6 +128,7 @@ const {
|
||||
popoverMinWidth?: string
|
||||
/** Maximum width of the popover (default: auto) */
|
||||
popoverMaxWidth?: string
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const selectedItem = defineModel<string | undefined>({ required: true })
|
||||
|
||||
91
src/composables/usePopoverSizing.test.ts
Normal file
91
src/composables/usePopoverSizing.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { effectScope } from 'vue'
|
||||
import type { EffectScope } from 'vue'
|
||||
|
||||
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
||||
|
||||
describe('usePrimeVueOverlayChildStyle', () => {
|
||||
let scope: EffectScope | undefined
|
||||
|
||||
function mountComposable() {
|
||||
scope = effectScope()
|
||||
let composable: ReturnType<typeof usePrimeVueOverlayChildStyle> | undefined
|
||||
|
||||
scope.run(() => {
|
||||
composable = usePrimeVueOverlayChildStyle()
|
||||
})
|
||||
|
||||
if (!composable) {
|
||||
throw new Error('Failed to mount composable')
|
||||
}
|
||||
|
||||
return composable
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
scope?.stop()
|
||||
scope = undefined
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('preserves existing stacking when there is no PrimeVue parent overlay', () => {
|
||||
const { overlayScopeRef, contentStyle } = mountComposable()
|
||||
|
||||
overlayScopeRef.value = document.createElement('div')
|
||||
|
||||
expect(contentStyle.value).toEqual({})
|
||||
})
|
||||
|
||||
it('renders above the closest PrimeVue dialog mask', () => {
|
||||
const { overlayScopeRef, contentStyle } = mountComposable()
|
||||
|
||||
overlayScopeRef.value = appendPrimeVueOverlay('p-dialog-mask', 5000)
|
||||
|
||||
expect(contentStyle.value).toEqual({ zIndex: 5001 })
|
||||
})
|
||||
|
||||
it('renders above the closest PrimeVue overlay mask', () => {
|
||||
const { overlayScopeRef, contentStyle } = mountComposable()
|
||||
|
||||
overlayScopeRef.value = appendPrimeVueOverlay('p-overlay-mask', 4200)
|
||||
|
||||
expect(contentStyle.value).toEqual({ zIndex: 4201 })
|
||||
})
|
||||
|
||||
it('does not drop below the Reka select overlay z-index floor', () => {
|
||||
const { overlayScopeRef, contentStyle } = mountComposable()
|
||||
|
||||
overlayScopeRef.value = appendPrimeVueOverlay('p-dialog-mask', 1200)
|
||||
|
||||
expect(contentStyle.value).toEqual({ zIndex: 3000 })
|
||||
})
|
||||
|
||||
it('preserves existing stacking when the PrimeVue overlay z-index is not numeric', () => {
|
||||
const { overlayScopeRef, contentStyle } = mountComposable()
|
||||
|
||||
overlayScopeRef.value = appendPrimeVueOverlay('p-dialog-mask')
|
||||
|
||||
expect(contentStyle.value).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
function appendPrimeVueOverlay(
|
||||
className: string,
|
||||
zIndex?: number
|
||||
): HTMLElement {
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = className
|
||||
if (zIndex !== undefined) {
|
||||
overlay.style.zIndex = String(zIndex)
|
||||
}
|
||||
|
||||
const anchor = document.createElement('div')
|
||||
overlay.append(anchor)
|
||||
document.body.append(overlay)
|
||||
|
||||
return anchor
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { computed } from 'vue'
|
||||
import type { CSSProperties, ComputedRef } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { CSSProperties, ComputedRef, Ref } from 'vue'
|
||||
|
||||
interface PopoverSizeOptions {
|
||||
minWidth?: string
|
||||
maxWidth?: string
|
||||
}
|
||||
|
||||
const PRIMEVUE_DIALOG_CHILD_Z_INDEX_FLOOR = 3000
|
||||
|
||||
/**
|
||||
* Composable for managing popover sizing styles
|
||||
* @param options Popover size configuration
|
||||
@@ -29,3 +31,30 @@ export function usePopoverSizing(
|
||||
return style
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps portaled Reka popovers above their containing PrimeVue dialog.
|
||||
*
|
||||
* This is a temporary bridge while PrimeVue dialogs and controls are
|
||||
* incrementally migrated to Reka UI. Once the affected PrimeVue parents are
|
||||
* migrated, this helper should be removed with the compatibility patch.
|
||||
*/
|
||||
export function usePrimeVueOverlayChildStyle(): {
|
||||
overlayScopeRef: Ref<HTMLElement | null>
|
||||
contentStyle: ComputedRef<CSSProperties>
|
||||
} {
|
||||
const overlayScopeRef = ref<HTMLElement | null>(null)
|
||||
const contentStyle = computed<CSSProperties>(() => {
|
||||
const overlay = overlayScopeRef.value?.closest(
|
||||
'.p-dialog-mask, .p-overlay-mask'
|
||||
)
|
||||
if (!overlay) return {}
|
||||
|
||||
const zIndex = Number.parseInt(getComputedStyle(overlay).zIndex, 10)
|
||||
if (!Number.isFinite(zIndex)) return {}
|
||||
|
||||
return { zIndex: Math.max(PRIMEVUE_DIALOG_CHILD_Z_INDEX_FLOOR, zIndex + 1) }
|
||||
})
|
||||
|
||||
return { overlayScopeRef, contentStyle }
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<template #header>
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
@click.self="focusedAsset = null"
|
||||
>
|
||||
@@ -52,6 +53,7 @@
|
||||
<AssetFilterBar
|
||||
:assets="categoryFilteredAssets"
|
||||
:show-ownership-filter
|
||||
:content-style="selectContentStyle"
|
||||
@filter-change="updateFilters"
|
||||
@click.self="focusedAsset = null"
|
||||
/>
|
||||
@@ -72,7 +74,12 @@
|
||||
</template>
|
||||
|
||||
<template #rightPanel>
|
||||
<ModelInfoPanel v-if="focusedAsset" :asset="focusedAsset" :cache-key />
|
||||
<ModelInfoPanel
|
||||
v-if="focusedAsset"
|
||||
:asset="focusedAsset"
|
||||
:cache-key
|
||||
:select-content-style="selectContentStyle"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full items-center justify-center p-6 text-center wrap-break-word text-muted"
|
||||
@@ -92,6 +99,7 @@ import SearchInput from '@/components/ui/search-input/SearchInput.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
||||
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
|
||||
import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
|
||||
import ModelInfoPanel from '@/platform/assets/components/modelInfo/ModelInfoPanel.vue'
|
||||
@@ -109,6 +117,8 @@ const { t } = useI18n()
|
||||
const assetStore = useAssetsStore()
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
||||
const selectContentStyle = primeVueOverlay.contentStyle
|
||||
|
||||
const props = defineProps<{
|
||||
nodeType?: string
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
v-model="activeFileFormatObjects"
|
||||
:label="$t('assetBrowser.fileFormats')"
|
||||
:options="availableFileFormats"
|
||||
:content-style="contentStyle"
|
||||
class="min-w-32"
|
||||
data-component-id="asset-filter-file-formats"
|
||||
@update:model-value="handleFilterChange"
|
||||
@@ -22,6 +23,7 @@
|
||||
v-model="activeBaseModelObjects"
|
||||
:label="$t('assetBrowser.baseModels')"
|
||||
:options="availableBaseModels"
|
||||
:content-style="contentStyle"
|
||||
class="min-w-32"
|
||||
data-component-id="asset-filter-base-models"
|
||||
@update:model-value="handleFilterChange"
|
||||
@@ -32,6 +34,7 @@
|
||||
v-model="ownership"
|
||||
:label="$t('assetBrowser.ownership')"
|
||||
:options="ownershipOptions"
|
||||
:content-style="contentStyle"
|
||||
class="min-w-32"
|
||||
data-component-id="asset-filter-ownership"
|
||||
@update:model-value="handleFilterChange"
|
||||
@@ -43,6 +46,7 @@
|
||||
v-model="sortBy"
|
||||
:label="$t('assetBrowser.sortBy')"
|
||||
:options="sortOptions"
|
||||
:content-style="contentStyle"
|
||||
class="min-w-32"
|
||||
data-component-id="asset-filter-sort"
|
||||
@update:model-value="handleFilterChange"
|
||||
@@ -57,6 +61,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MultiSelect from '@/components/ui/multi-select/MultiSelect.vue'
|
||||
@@ -78,9 +83,14 @@ const sortOptions = computed(() => [
|
||||
{ name: t('assetBrowser.sortZA'), value: 'name-desc' as const }
|
||||
])
|
||||
|
||||
const { assets = [], showOwnershipFilter = false } = defineProps<{
|
||||
const {
|
||||
assets = [],
|
||||
showOwnershipFilter = false,
|
||||
contentStyle
|
||||
} = defineProps<{
|
||||
assets?: AssetItem[]
|
||||
showOwnershipFilter?: boolean
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const selectedFileFormats = ref<SelectOption[]>([])
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 text-sm text-muted-foreground">
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex flex-col gap-4 text-sm text-muted-foreground"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">
|
||||
{{ $t('assetBrowser.modelAssociatedWithLink') }}
|
||||
@@ -39,6 +42,7 @@
|
||||
"
|
||||
:options="modelTypes"
|
||||
:disabled="isLoading"
|
||||
:content-style="selectContentStyle"
|
||||
data-attr="upload-model-step2-type-selector"
|
||||
/>
|
||||
</div>
|
||||
@@ -47,6 +51,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import SingleSelect from '@/components/ui/single-select/SingleSelect.vue'
|
||||
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
||||
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
|
||||
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
|
||||
|
||||
@@ -58,4 +63,6 @@ defineProps<{
|
||||
const modelValue = defineModel<string | undefined>()
|
||||
|
||||
const { modelTypes, isLoading } = useModelTypes()
|
||||
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
||||
const selectContentStyle = primeVueOverlay.contentStyle
|
||||
</script>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
:placeholder="t('assetBrowser.modelInfo.selectModelType')"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent :style="selectContentStyle">
|
||||
<SelectItem
|
||||
v-for="option in modelTypes"
|
||||
:key="option.value"
|
||||
@@ -210,6 +210,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
@@ -257,9 +258,10 @@ const accordionClass = cn(
|
||||
'border-t border-border-default bg-modal-panel-background'
|
||||
)
|
||||
|
||||
const { asset, cacheKey } = defineProps<{
|
||||
const { asset, cacheKey, selectContentStyle } = defineProps<{
|
||||
asset: AssetDisplayItem
|
||||
cacheKey?: string
|
||||
selectContentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const assetsStore = useAssetsStore()
|
||||
|
||||
@@ -14,16 +14,21 @@
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<div class="flex w-full items-center justify-between gap-2">
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
>
|
||||
<div class="flex w-full items-center gap-2">
|
||||
<SingleSelect
|
||||
v-model="searchMode"
|
||||
class="min-w-34"
|
||||
:options="filterOptions"
|
||||
:content-style="selectContentStyle"
|
||||
/>
|
||||
<SearchAutocomplete
|
||||
v-model="searchQuery"
|
||||
:suggestions="suggestions"
|
||||
:content-style="selectContentStyle"
|
||||
:placeholder="$t('manager.searchPlaceholder')"
|
||||
option-label="query"
|
||||
autofocus
|
||||
@@ -87,6 +92,7 @@
|
||||
v-model="sortField"
|
||||
:label="$t('g.sort')"
|
||||
:options="availableSortOptions"
|
||||
:content-style="selectContentStyle"
|
||||
class="w-48"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -163,6 +169,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { usePrimeVueOverlayChildStyle } from '@/composables/usePopoverSizing'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
@@ -197,6 +204,8 @@ const { initialTab, initialPackId, onClose } = defineProps<{
|
||||
provide(OnCloseKey, onClose)
|
||||
|
||||
const { t } = useI18n()
|
||||
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
||||
const selectContentStyle = primeVueOverlay.contentStyle
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { getPackById } = useComfyRegistryStore()
|
||||
|
||||
Reference in New Issue
Block a user