mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-27 15:57:48 +00:00
Compare commits
18 Commits
bl/com-304
...
fix/dropdo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b66d21a6 | ||
|
|
41e426fe7e | ||
|
|
47c9a027a7 | ||
|
|
6fbc5723bd | ||
|
|
530cef855b | ||
|
|
66e42f038b | ||
|
|
2077fa76e7 | ||
|
|
e5bc943487 | ||
|
|
b96b56d771 | ||
|
|
8894119dc9 | ||
|
|
d2345fc7eb | ||
|
|
8a88e40c40 | ||
|
|
0def631c52 | ||
|
|
7b5a49975f | ||
|
|
3d0389ac5b | ||
|
|
049657b38f | ||
|
|
b5bae1f721 | ||
|
|
59f4ed8232 |
@@ -74,7 +74,9 @@ export const TestIds = {
|
||||
decrement: 'decrement',
|
||||
increment: 'increment',
|
||||
domWidgetTextarea: 'dom-widget-textarea',
|
||||
subgraphEnterButton: 'subgraph-enter-button'
|
||||
subgraphEnterButton: 'subgraph-enter-button',
|
||||
formDropdownMenu: 'form-dropdown-menu',
|
||||
formDropdownTrigger: 'form-dropdown-trigger'
|
||||
},
|
||||
builder: {
|
||||
ioItem: 'builder-io-item',
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '../fixtures/ComfyPage'
|
||||
import { TestIds } from '../fixtures/selectors'
|
||||
|
||||
/**
|
||||
* Default workflow widget inputs as [nodeId, widgetName] tuples.
|
||||
@@ -143,15 +144,12 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
|
||||
const dropdownButton = imageRow.locator('button:has(> span)').first()
|
||||
await dropdownButton.click()
|
||||
|
||||
// The unstyled PrimeVue Popover renders with role="dialog".
|
||||
// Locate the one containing the image grid (filter buttons like "All", "Inputs").
|
||||
const popover = comfyPage.page
|
||||
.getByRole('dialog')
|
||||
.filter({ has: comfyPage.page.getByRole('button', { name: 'All' }) })
|
||||
const menu = comfyPage.page
|
||||
.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||
.first()
|
||||
await expect(popover).toBeVisible({ timeout: 5000 })
|
||||
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const isInViewport = await popover.evaluate((el) => {
|
||||
const isInViewport = await menu.evaluate((el) => {
|
||||
const rect = el.getBoundingClientRect()
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
@@ -162,7 +160,7 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
|
||||
})
|
||||
expect(isInViewport).toBe(true)
|
||||
|
||||
const isClipped = await popover.evaluate(isClippedByAnyAncestor)
|
||||
const isClipped = await menu.evaluate(isClippedByAnyAncestor)
|
||||
expect(isClipped).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { TestIds } from '../../../../fixtures/selectors'
|
||||
|
||||
test.describe(
|
||||
'FormDropdown positioning in Vue nodes',
|
||||
{ tag: ['@widget', '@node'] },
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('dropdown menu appears directly below the trigger', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||
await trigger.first().click()
|
||||
|
||||
const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const triggerBox = await trigger.first().boundingBox()
|
||||
const menuBox = await menu.boundingBox()
|
||||
|
||||
expect(triggerBox).toBeTruthy()
|
||||
expect(menuBox).toBeTruthy()
|
||||
|
||||
// Menu top should be near the trigger bottom (within 20px tolerance for padding)
|
||||
expect(menuBox!.y).toBeGreaterThanOrEqual(
|
||||
triggerBox!.y + triggerBox!.height - 5
|
||||
)
|
||||
expect(menuBox!.y).toBeLessThanOrEqual(
|
||||
triggerBox!.y + triggerBox!.height + 20
|
||||
)
|
||||
|
||||
// Menu left should be near the trigger left (within 10px tolerance)
|
||||
expect(menuBox!.x).toBeGreaterThanOrEqual(triggerBox!.x - 10)
|
||||
expect(menuBox!.x).toBeLessThanOrEqual(triggerBox!.x + 10)
|
||||
})
|
||||
|
||||
test('dropdown menu appears correctly at different zoom levels', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
for (const zoom of [0.75, 1.5]) {
|
||||
// Set zoom via canvas
|
||||
await comfyPage.page.evaluate((scale) => {
|
||||
const canvas = window.app!.canvas
|
||||
canvas.ds.scale = scale
|
||||
canvas.setDirty(true, true)
|
||||
}, zoom)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||
await trigger.first().click()
|
||||
|
||||
const menu = comfyPage.page.getByTestId(
|
||||
TestIds.widgets.formDropdownMenu
|
||||
)
|
||||
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||
|
||||
const triggerBox = await trigger.first().boundingBox()
|
||||
const menuBox = await menu.boundingBox()
|
||||
|
||||
expect(triggerBox).toBeTruthy()
|
||||
expect(menuBox).toBeTruthy()
|
||||
|
||||
// Menu top should still be near trigger bottom regardless of zoom
|
||||
expect(menuBox!.y).toBeGreaterThanOrEqual(
|
||||
triggerBox!.y + triggerBox!.height - 5
|
||||
)
|
||||
expect(menuBox!.y).toBeLessThanOrEqual(
|
||||
triggerBox!.y + triggerBox!.height + 20 * zoom
|
||||
)
|
||||
|
||||
// Close dropdown before next iteration
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(menu).not.toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('dropdown closes on outside click', async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||
await trigger.first().click()
|
||||
|
||||
const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Click outside the node
|
||||
await comfyPage.page.mouse.click(10, 10)
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('dropdown closes on Escape key', async ({ comfyPage }) => {
|
||||
const node = comfyPage.vueNodes.getNodeByTitle('Load Image')
|
||||
const trigger = node.getByTestId(TestIds.widgets.formDropdownTrigger)
|
||||
await trigger.first().click()
|
||||
|
||||
const menu = comfyPage.page.getByTestId(TestIds.widgets.formDropdownMenu)
|
||||
await expect(menu).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(menu).not.toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -141,6 +141,7 @@ import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
|
||||
import IconGroup from '@/components/button/IconGroup.vue'
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import {
|
||||
formatDuration,
|
||||
@@ -279,7 +280,8 @@ const formattedDuration = computed(() => {
|
||||
// Get metadata info based on file kind
|
||||
const metaInfo = computed(() => {
|
||||
if (!asset) return ''
|
||||
if (fileKind.value === 'image' && imageDimensions.value) {
|
||||
// TODO(assets): Re-enable once /assets API returns original image dimensions in metadata (#10590)
|
||||
if (fileKind.value === 'image' && imageDimensions.value && !isCloud) {
|
||||
return `${imageDimensions.value.width}x${imageDimensions.value.height}`
|
||||
}
|
||||
if (asset.size && ['video', 'audio', '3D'].includes(fileKind.value)) {
|
||||
|
||||
@@ -300,6 +300,25 @@ describe('TeamWorkspacesDialogContent', () => {
|
||||
|
||||
expect(mockCreateWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resets loading state after createWorkspace fails', async () => {
|
||||
mockCreateWorkspace.mockRejectedValue(new Error('Limit reached'))
|
||||
const wrapper = mountComponent()
|
||||
|
||||
await typeAndCreate(wrapper, 'New Team')
|
||||
|
||||
expect(findCreateButton(wrapper).props('loading')).toBe(false)
|
||||
})
|
||||
|
||||
it('resets loading state after onConfirm fails', async () => {
|
||||
mockCreateWorkspace.mockResolvedValue({ id: 'new-ws' })
|
||||
const onConfirm = vi.fn().mockRejectedValue(new Error('Setup failed'))
|
||||
const wrapper = mountComponent({ onConfirm })
|
||||
|
||||
await typeAndCreate(wrapper, 'New Team')
|
||||
|
||||
expect(findCreateButton(wrapper).props('loading')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('close button', () => {
|
||||
|
||||
@@ -201,28 +201,30 @@ async function handleSwitch(workspaceId: string) {
|
||||
async function onCreate() {
|
||||
if (!isValidName.value || loading.value) return
|
||||
loading.value = true
|
||||
const name = workspaceName.value.trim()
|
||||
try {
|
||||
await workspaceStore.createWorkspace(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
const name = workspaceName.value.trim()
|
||||
try {
|
||||
await workspaceStore.createWorkspace(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await onConfirm?.(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('teamWorkspacesDialog.confirmCallbackFailed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
}
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
} finally {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
await onConfirm?.(name)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('teamWorkspacesDialog.confirmCallbackFailed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
}
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -41,11 +41,6 @@ const MockFormDropdownInput = {
|
||||
'<button class="mock-dropdown-trigger" @click="$emit(\'select-click\', $event)">Open</button>'
|
||||
}
|
||||
|
||||
const MockPopover = {
|
||||
name: 'Popover',
|
||||
template: '<div><slot /></div>'
|
||||
}
|
||||
|
||||
interface MountDropdownOptions {
|
||||
searcher?: (
|
||||
query: string,
|
||||
@@ -65,13 +60,17 @@ function mountDropdown(
|
||||
plugins: [PrimeVue, i18n],
|
||||
stubs: {
|
||||
FormDropdownInput: MockFormDropdownInput,
|
||||
Popover: MockPopover,
|
||||
FormDropdownMenu: MockFormDropdownMenu
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function openDropdown(wrapper: ReturnType<typeof mountDropdown>) {
|
||||
await wrapper.find('.mock-dropdown-trigger').trigger('click')
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
function getMenuItems(
|
||||
wrapper: ReturnType<typeof mountDropdown>
|
||||
): FormDropdownItem[] {
|
||||
@@ -87,7 +86,7 @@ describe('FormDropdown', () => {
|
||||
createItem('input-0', 'video1.mp4'),
|
||||
createItem('input-1', 'video2.mp4')
|
||||
])
|
||||
await flushPromises()
|
||||
await openDropdown(wrapper)
|
||||
|
||||
expect(getMenuItems(wrapper)).toHaveLength(2)
|
||||
|
||||
@@ -106,7 +105,7 @@ describe('FormDropdown', () => {
|
||||
|
||||
it('updates when items change but IDs stay the same', async () => {
|
||||
const wrapper = mountDropdown([createItem('1', 'alpha')])
|
||||
await flushPromises()
|
||||
await openDropdown(wrapper)
|
||||
|
||||
await wrapper.setProps({ items: [createItem('1', 'beta')] })
|
||||
await flushPromises()
|
||||
@@ -116,7 +115,7 @@ describe('FormDropdown', () => {
|
||||
|
||||
it('updates when switching between empty and non-empty items', async () => {
|
||||
const wrapper = mountDropdown([])
|
||||
await flushPromises()
|
||||
await openDropdown(wrapper)
|
||||
|
||||
expect(getMenuItems(wrapper)).toHaveLength(0)
|
||||
|
||||
@@ -154,7 +153,10 @@ describe('FormDropdown', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(searcher).not.toHaveBeenCalled()
|
||||
expect(getMenuItems(wrapper).map((item) => item.id)).toEqual(['3', '4'])
|
||||
|
||||
await openDropdown(wrapper)
|
||||
|
||||
expect(searcher).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('runs filtering when dropdown opens', async () => {
|
||||
@@ -169,8 +171,7 @@ describe('FormDropdown', () => {
|
||||
)
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('.mock-dropdown-trigger').trigger('click')
|
||||
await flushPromises()
|
||||
await openDropdown(wrapper)
|
||||
|
||||
expect(searcher).toHaveBeenCalled()
|
||||
expect(getMenuItems(wrapper).map((item) => item.id)).toEqual(['keep'])
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computedAsync, refDebounced } from '@vueuse/core'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { computedAsync, onClickOutside, refDebounced } from '@vueuse/core'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, inject, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { OverlayAppendToKey } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type {
|
||||
FilterOption,
|
||||
@@ -16,6 +17,7 @@ import type {
|
||||
import FormDropdownInput from './FormDropdownInput.vue'
|
||||
import FormDropdownMenu from './FormDropdownMenu.vue'
|
||||
import { defaultSearcher, getDefaultSortOptions } from './shared'
|
||||
import { MENU_HEIGHT, MENU_WIDTH } from './types'
|
||||
import type { FormDropdownItem, LayoutMode, SortOption } from './types'
|
||||
|
||||
interface Props {
|
||||
@@ -51,7 +53,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const overlayProps = useTransformCompatOverlayProps()
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
@@ -95,8 +96,10 @@ const baseModelSelected = defineModel<Set<string>>('baseModelSelected', {
|
||||
const isOpen = defineModel<boolean>('isOpen', { default: false })
|
||||
|
||||
const toastStore = useToastStore()
|
||||
const popoverRef = ref<InstanceType<typeof Popover>>()
|
||||
const triggerRef = useTemplateRef('triggerRef')
|
||||
const dropdownRef = useTemplateRef('dropdownRef')
|
||||
|
||||
const shouldTeleport = inject(OverlayAppendToKey, undefined) === 'body'
|
||||
|
||||
const maxSelectable = computed(() => {
|
||||
if (multiple === true) return Infinity
|
||||
@@ -142,18 +145,59 @@ function internalIsSelected(item: FormDropdownItem, index: number): boolean {
|
||||
return isSelected(selected.value, item, index)
|
||||
}
|
||||
|
||||
const toggleDropdown = (event: Event) => {
|
||||
const MENU_HEIGHT_WITH_GAP = MENU_HEIGHT + 8
|
||||
const openUpward = ref(false)
|
||||
const fixedPosition = ref({ top: 0, left: 0 })
|
||||
|
||||
const teleportStyle = computed<CSSProperties | undefined>(() => {
|
||||
if (!shouldTeleport) return undefined
|
||||
const pos = fixedPosition.value
|
||||
return openUpward.value
|
||||
? {
|
||||
position: 'fixed',
|
||||
left: `${pos.left}px`,
|
||||
bottom: `${window.innerHeight - pos.top}px`,
|
||||
paddingBottom: '0.5rem'
|
||||
}
|
||||
: {
|
||||
position: 'fixed',
|
||||
left: `${pos.left}px`,
|
||||
top: `${pos.top}px`,
|
||||
paddingTop: '0.5rem'
|
||||
}
|
||||
})
|
||||
|
||||
function toggleDropdown() {
|
||||
if (disabled) return
|
||||
if (popoverRef.value && triggerRef.value) {
|
||||
popoverRef.value.toggle?.(event, triggerRef.value)
|
||||
isOpen.value = !isOpen.value
|
||||
if (!isOpen.value && triggerRef.value) {
|
||||
const rect = triggerRef.value.getBoundingClientRect()
|
||||
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const spaceAbove = rect.top
|
||||
openUpward.value =
|
||||
spaceBelow < MENU_HEIGHT_WITH_GAP && spaceAbove > spaceBelow
|
||||
|
||||
if (shouldTeleport) {
|
||||
fixedPosition.value = {
|
||||
top: openUpward.value
|
||||
? Math.max(MENU_HEIGHT_WITH_GAP, rect.top)
|
||||
: Math.min(rect.bottom, window.innerHeight - MENU_HEIGHT_WITH_GAP),
|
||||
left: Math.min(rect.right, window.innerWidth - MENU_WIDTH)
|
||||
}
|
||||
}
|
||||
}
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const closeDropdown = () => {
|
||||
if (popoverRef.value) {
|
||||
popoverRef.value.hide?.()
|
||||
isOpen.value = false
|
||||
function closeDropdown() {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
onClickOutside(triggerRef, closeDropdown, { ignore: [dropdownRef] })
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +236,7 @@ function handleSelection(item: FormDropdownItem, index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="triggerRef">
|
||||
<div ref="triggerRef" class="relative" @keydown="handleEscape">
|
||||
<FormDropdownInput
|
||||
:files
|
||||
:is-open
|
||||
@@ -207,42 +251,41 @@ function handleSelection(item: FormDropdownItem, index: number) {
|
||||
@select-click="toggleDropdown"
|
||||
@file-change="handleFileChange"
|
||||
/>
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
:append-to="overlayProps.appendTo"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class: 'absolute z-50'
|
||||
},
|
||||
content: {
|
||||
class: ['bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg']
|
||||
}
|
||||
}"
|
||||
@hide="isOpen = false"
|
||||
>
|
||||
<FormDropdownMenu
|
||||
v-model:filter-selected="filterSelected"
|
||||
v-model:layout-mode="layoutMode"
|
||||
v-model:sort-selected="sortSelected"
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:ownership-selected="ownershipSelected"
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:filter-options
|
||||
:sort-options
|
||||
:show-ownership-filter
|
||||
:ownership-options
|
||||
:show-base-model-filter
|
||||
:base-model-options
|
||||
:disabled
|
||||
:items="sortedItems"
|
||||
:is-selected="internalIsSelected"
|
||||
:max-selectable
|
||||
@close="closeDropdown"
|
||||
@item-click="handleSelection"
|
||||
/>
|
||||
</Popover>
|
||||
<Teleport to="body" :disabled="!shouldTeleport">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
ref="dropdownRef"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 rounded-lg border-none bg-transparent p-0 shadow-lg',
|
||||
!shouldTeleport && 'absolute left-0',
|
||||
!shouldTeleport &&
|
||||
(openUpward ? 'bottom-full pb-2' : 'top-full pt-2')
|
||||
)
|
||||
"
|
||||
:style="teleportStyle"
|
||||
>
|
||||
<FormDropdownMenu
|
||||
v-model:filter-selected="filterSelected"
|
||||
v-model:layout-mode="layoutMode"
|
||||
v-model:sort-selected="sortSelected"
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:ownership-selected="ownershipSelected"
|
||||
v-model:base-model-selected="baseModelSelected"
|
||||
:filter-options
|
||||
:sort-options
|
||||
:show-ownership-filter
|
||||
:ownership-options
|
||||
:show-base-model-filter
|
||||
:base-model-options
|
||||
:disabled
|
||||
:items="sortedItems"
|
||||
:is-selected="internalIsSelected"
|
||||
:max-selectable
|
||||
@close="closeDropdown"
|
||||
@item-click="handleSelection"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -61,6 +61,7 @@ const theButtonStyle = computed(() =>
|
||||
"
|
||||
>
|
||||
<button
|
||||
data-testid="form-dropdown-trigger"
|
||||
:class="
|
||||
cn(
|
||||
theButtonStyle,
|
||||
|
||||
@@ -97,6 +97,7 @@ const virtualItems = computed<VirtualDropdownItem[]>(() =>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-testid="form-dropdown-menu"
|
||||
class="flex h-[640px] w-103 flex-col rounded-lg bg-component-node-background pt-4 outline -outline-offset-1 outline-node-component-border"
|
||||
>
|
||||
<FormDropdownMenuFilter
|
||||
|
||||
@@ -28,5 +28,10 @@ export interface SortOption<TId extends string = string> {
|
||||
|
||||
export type LayoutMode = 'list' | 'grid' | 'list-small'
|
||||
|
||||
/** Height of FormDropdownMenu in pixels (matches h-[640px] in template). */
|
||||
export const MENU_HEIGHT = 640
|
||||
/** Width of FormDropdownMenu in pixels (matches w-103 = 26rem = 416px in template). */
|
||||
export const MENU_WIDTH = 412
|
||||
|
||||
export const AssetKindKey: InjectionKey<ComputedRef<AssetKind | undefined>> =
|
||||
Symbol('assetKind')
|
||||
|
||||
Reference in New Issue
Block a user