Compare commits
2 Commits
main
...
version-bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65ba0723a1 | ||
|
|
7e8b7959f2 |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 100 KiB |
@@ -13,7 +13,7 @@ const { stars } = defineProps<{
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="`ComfyUI on GitHub — ${stars} stars`"
|
||||
class="hidden shrink-0 items-center gap-1 lg:flex"
|
||||
class="hidden shrink-0 items-center gap-2 lg:flex"
|
||||
>
|
||||
<NodeBadge
|
||||
:segments="[{ text: stars }]"
|
||||
@@ -22,7 +22,7 @@ const { stars } = defineProps<{
|
||||
size-class="h-5 sm:h-5"
|
||||
/>
|
||||
<span
|
||||
class="bg-primary-comfy-yellow block size-6 shrink-0"
|
||||
class="bg-primary-comfy-yellow block size-7"
|
||||
aria-hidden="true"
|
||||
style="mask: url('/icons/social/github.svg') center / contain no-repeat"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.45.0",
|
||||
"version": "1.44.19",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -40,10 +40,7 @@
|
||||
|
||||
<template #contentFilter>
|
||||
<div class="relative flex flex-wrap justify-between gap-2 px-6 pb-4">
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Model Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedModelObjects"
|
||||
@@ -51,7 +48,6 @@
|
||||
class="w-[250px]"
|
||||
:label="modelFilterLabel"
|
||||
:options="modelOptions"
|
||||
:content-style="selectContentStyle"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
@@ -66,7 +62,6 @@
|
||||
v-model="selectedUseCaseObjects"
|
||||
:label="useCaseFilterLabel"
|
||||
:options="useCaseOptions"
|
||||
:content-style="selectContentStyle"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
@@ -81,7 +76,6 @@
|
||||
v-model="selectedRunsOnObjects"
|
||||
:label="runsOnFilterLabel"
|
||||
:options="runsOnOptions"
|
||||
:content-style="selectContentStyle"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
@@ -98,7 +92,6 @@
|
||||
v-model="sortBy"
|
||||
:label="$t('templateWorkflows.sorting', 'Sort by')"
|
||||
:options="sortOptions"
|
||||
:content-style="selectContentStyle"
|
||||
class="w-62.5"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -423,7 +416,6 @@ 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'
|
||||
@@ -640,8 +632,6 @@ 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,8 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="keybinding-panel flex flex-col gap-2"
|
||||
>
|
||||
<div class="keybinding-panel flex flex-col gap-2">
|
||||
<Teleport defer to="#keybinding-panel-header">
|
||||
<SearchInput
|
||||
v-model="filters['global'].value"
|
||||
@@ -18,12 +15,10 @@
|
||||
<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"
|
||||
@@ -243,7 +238,6 @@
|
||||
</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
|
||||
@@ -320,7 +314,6 @@ 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'
|
||||
@@ -344,8 +337,6 @@ const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const { t } = useI18n()
|
||||
const primeVueOverlay = usePrimeVueOverlayChildStyle()
|
||||
const keybindingOverlayContentStyle = primeVueOverlay.contentStyle
|
||||
|
||||
const presetNames = ref<string[]>([])
|
||||
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
{{ displayLabel }}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
:style="contentStyle"
|
||||
class="max-w-64 min-w-0 **:[[role=listbox]]:gap-1"
|
||||
>
|
||||
<SelectContent class="max-w-64 min-w-0 **:[[role=listbox]]:gap-1">
|
||||
<div class="max-w-60">
|
||||
<SelectItem
|
||||
value="default"
|
||||
@@ -49,7 +46,6 @@
|
||||
|
||||
<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'
|
||||
@@ -61,9 +57,8 @@ import SelectValue from '@/components/ui/select/SelectValue.vue'
|
||||
import { useKeybindingPresetService } from '@/platform/keybindings/presetService'
|
||||
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
|
||||
|
||||
const { presetNames, contentStyle } = defineProps<{
|
||||
const { presetNames } = defineProps<{
|
||||
presetNames: string[]
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||
|
||||
@@ -21,19 +21,13 @@ vi.mock('@/components/common/LazyImage.vue', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const mockRect = (el: HTMLElement, width: number) => {
|
||||
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: width,
|
||||
bottom: 100,
|
||||
width,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
} as DOMRect)
|
||||
}
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useMouseInElement: () => ({
|
||||
elementX: ref(50),
|
||||
elementWidth: ref(100),
|
||||
isOutside: ref(false)
|
||||
})
|
||||
}))
|
||||
|
||||
describe('CompareSliderThumbnail', () => {
|
||||
const renderThumbnail = (props = {}) => {
|
||||
@@ -80,44 +74,4 @@ describe('CompareSliderThumbnail', () => {
|
||||
const divider = screen.getByTestId('compare-slider-divider')
|
||||
expect(divider.style.left).toBe('50%')
|
||||
})
|
||||
|
||||
it('updates slider position on mousemove', async () => {
|
||||
renderThumbnail()
|
||||
const container = screen.getByTestId('compare-slider-container')
|
||||
mockRect(container, 200)
|
||||
|
||||
const user = userEvent.setup()
|
||||
await user.pointer({ target: container, coords: { clientX: 50 } })
|
||||
|
||||
const divider = screen.getByTestId('compare-slider-divider')
|
||||
expect(divider.style.left).toBe('25%')
|
||||
})
|
||||
|
||||
it('clamps slider position to [0, 100] when pointer overshoots', async () => {
|
||||
renderThumbnail()
|
||||
const container = screen.getByTestId('compare-slider-container')
|
||||
mockRect(container, 200)
|
||||
|
||||
const user = userEvent.setup()
|
||||
|
||||
await user.pointer({ target: container, coords: { clientX: -10 } })
|
||||
let divider = screen.getByTestId('compare-slider-divider')
|
||||
expect(divider.style.left).toBe('0%')
|
||||
|
||||
await user.pointer({ target: container, coords: { clientX: 250 } })
|
||||
divider = screen.getByTestId('compare-slider-divider')
|
||||
expect(divider.style.left).toBe('100%')
|
||||
})
|
||||
|
||||
it('ignores mousemove when container has zero width', async () => {
|
||||
renderThumbnail()
|
||||
const container = screen.getByTestId('compare-slider-container')
|
||||
mockRect(container, 0)
|
||||
|
||||
const user = userEvent.setup()
|
||||
await user.pointer({ target: container, coords: { clientX: 50 } })
|
||||
|
||||
const divider = screen.getByTestId('compare-slider-divider')
|
||||
expect(divider.style.left).toBe('50%')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
: 'max-w-full max-h-64 object-contain'
|
||||
"
|
||||
/>
|
||||
<div
|
||||
data-testid="compare-slider-container"
|
||||
class="absolute inset-0"
|
||||
@mousemove="updateSliderPosition"
|
||||
>
|
||||
<div ref="containerRef" class="absolute inset-0">
|
||||
<LazyImage
|
||||
:src="overlayImageSrc"
|
||||
:alt="alt"
|
||||
@@ -38,7 +34,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useMouseInElement } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import LazyImage from '@/components/common/LazyImage.vue'
|
||||
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
|
||||
@@ -60,20 +57,18 @@ const isVideoType =
|
||||
false
|
||||
|
||||
const sliderPosition = ref(SLIDER_START_POSITION)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
/**
|
||||
* Update slider position from a local mousemove. Scoped to currentTarget so
|
||||
* only the hovered card reads its rect — unlike useMouseInElement which
|
||||
* attaches a global mousemove listener and fires for every mounted instance.
|
||||
*/
|
||||
function updateSliderPosition(event: MouseEvent) {
|
||||
const el = event.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
if (rect.width === 0) return
|
||||
// Clamp to [0, 100] — subpixel rounding or stale rects on hover-in can
|
||||
// push the raw percentage slightly out of range, which would offset the
|
||||
// divider past the container or invert the overlay's clipPath.
|
||||
const raw = ((event.clientX - rect.left) / rect.width) * 100
|
||||
sliderPosition.value = Math.max(0, Math.min(100, raw))
|
||||
}
|
||||
const { elementX, elementWidth, isOutside } = useMouseInElement(containerRef)
|
||||
|
||||
// Update slider position based on mouse position when hovered
|
||||
watch(
|
||||
[() => isHovered, elementX, elementWidth, isOutside],
|
||||
([isHovered, x, width, outside]) => {
|
||||
if (!isHovered) return
|
||||
if (!outside) {
|
||||
sliderPosition.value = (x / width) * 100
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
position="popper"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
:style="[popoverStyle, contentStyle]"
|
||||
:style="popoverStyle"
|
||||
:class="selectContentClass"
|
||||
@keydown="onContentKeydown"
|
||||
@focus-outside="preventFocusDismiss"
|
||||
@@ -152,7 +152,6 @@ 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'
|
||||
@@ -184,8 +183,7 @@ const {
|
||||
searchPlaceholder,
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth,
|
||||
contentStyle
|
||||
popoverMaxWidth
|
||||
} = defineProps<{
|
||||
/** Input label shown on the trigger button */
|
||||
label?: string
|
||||
@@ -209,7 +207,6 @@ const {
|
||||
popoverMinWidth?: string
|
||||
/** Maximum width of the popover (default: auto) */
|
||||
popoverMaxWidth?: string
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const selectedItems = defineModel<SelectOption[]>({
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
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',
|
||||
@@ -100,7 +99,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { HTMLAttributes, StyleValue } from 'vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import {
|
||||
@@ -133,8 +132,7 @@ const {
|
||||
suggestions = [],
|
||||
optionLabel,
|
||||
optionKey,
|
||||
class: className,
|
||||
contentStyle
|
||||
class: className
|
||||
} = defineProps<{
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
@@ -146,7 +144,6 @@ 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, contentStyle]"
|
||||
:style="optionStyle"
|
||||
:class="cn(selectContentClass, 'min-w-(--reka-select-trigger-width)')"
|
||||
@keydown="onContentKeydown"
|
||||
>
|
||||
@@ -82,7 +82,6 @@ import {
|
||||
SelectViewport
|
||||
} from 'reka-ui'
|
||||
import { ref } from 'vue'
|
||||
import type { StyleValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
@@ -109,8 +108,7 @@ const {
|
||||
disabled = false,
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth,
|
||||
contentStyle
|
||||
popoverMaxWidth
|
||||
} = defineProps<{
|
||||
label?: string
|
||||
options?: SelectOption[]
|
||||
@@ -128,7 +126,6 @@ const {
|
||||
popoverMinWidth?: string
|
||||
/** Maximum width of the popover (default: auto) */
|
||||
popoverMaxWidth?: string
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const selectedItem = defineModel<string | undefined>({ required: true })
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
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,14 +1,11 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { CSSProperties, ComputedRef, Ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import type { CSSProperties, ComputedRef } from 'vue'
|
||||
|
||||
interface PopoverSizeOptions {
|
||||
minWidth?: string
|
||||
maxWidth?: string
|
||||
}
|
||||
|
||||
// Matches the highest existing Reka popover z-index (e.g. z-3000 on SearchAutocomplete).
|
||||
const PRIMEVUE_DIALOG_CHILD_Z_INDEX_FLOOR = 3000
|
||||
|
||||
/**
|
||||
* Composable for managing popover sizing styles
|
||||
* @param options Popover size configuration
|
||||
@@ -32,30 +29,3 @@ 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 }
|
||||
}
|
||||
|
||||
@@ -17937,7 +17937,7 @@
|
||||
"inputs": {
|
||||
"warped_noise": {
|
||||
"name": "warped_noise",
|
||||
"tooltip": "الضجيج المشوه (latent) من VOIDWarpedNoise"
|
||||
"tooltip": "الضجيج المشوه (warped noise latent) من VOIDWarpedNoise"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
|
||||
@@ -17919,7 +17919,7 @@
|
||||
},
|
||||
"video": {
|
||||
"name": "video",
|
||||
"tooltip": "Salida de fotogramas de video de la pasada 1 [T, H, W, 3]"
|
||||
"tooltip": "Fotogramas de video de salida de la pasada 1 [T, H, W, 3]"
|
||||
},
|
||||
"width": {
|
||||
"name": "width"
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
},
|
||||
"login": {
|
||||
"andText": "و",
|
||||
"backToGithubLogin": "ثبتنام با گیتهاب",
|
||||
"backToGithubLogin": "بهجای آن با Github ثبتنام کنید",
|
||||
"backToLogin": "بازگشت به ورود",
|
||||
"backToSocialLogin": "ثبتنام با Google یا Github",
|
||||
"confirmPasswordLabel": "تأیید رمز عبور",
|
||||
|
||||
@@ -17879,7 +17879,7 @@
|
||||
"inputs": {
|
||||
"dilate_width": {
|
||||
"name": "dilate_width",
|
||||
"tooltip": "شعاع گسترش برای ناحیه اصلی mask (۰ = بدون گسترش)"
|
||||
"tooltip": "شعاع گسترش برای ناحیه اصلی ماسک (۰ = بدون گسترش)"
|
||||
},
|
||||
"mask": {
|
||||
"name": "mask"
|
||||
@@ -17919,7 +17919,7 @@
|
||||
},
|
||||
"video": {
|
||||
"name": "video",
|
||||
"tooltip": "خروجی فریمهای ویدیو از مرحله ۱ [T, H, W, ۳]"
|
||||
"tooltip": "خروجی فریمهای ویدیو از مرحله اول [T, H, W, ۳]"
|
||||
},
|
||||
"width": {
|
||||
"name": "width"
|
||||
@@ -17937,7 +17937,7 @@
|
||||
"inputs": {
|
||||
"warped_noise": {
|
||||
"name": "warped_noise",
|
||||
"tooltip": "latent نویز تغییرشکلیافته از VOIDWarpedNoise"
|
||||
"tooltip": "لاتنت نویز تغییرشکلیافته از VOIDWarpedNoise"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
},
|
||||
"login": {
|
||||
"andText": "et",
|
||||
"backToGithubLogin": "S'inscrire avec Github à la place",
|
||||
"backToGithubLogin": "S’inscrire avec Github à la place",
|
||||
"backToLogin": "Retour à la connexion",
|
||||
"backToSocialLogin": "Inscrivez-vous avec Google ou Github à la place",
|
||||
"confirmPasswordLabel": "Confirmer le mot de passe",
|
||||
|
||||
@@ -17836,7 +17836,7 @@
|
||||
},
|
||||
"length": {
|
||||
"name": "length",
|
||||
"tooltip": "処理するピクセルフレーム数。CogVideoX-Fun-V1.5(patch_size_t=2)の場合、latent_tは偶数でなければなりません — 奇数のlatent_tになる長さは切り捨てられます(例:49 → 45)。"
|
||||
"tooltip": "処理するピクセルフレーム数。CogVideoX-Fun-V1.5(patch_size_t=2)の場合、latent_tは偶数でなければなりません。奇数のlatent_tになる長さは切り捨てられます(例:49 → 45)。"
|
||||
},
|
||||
"negative": {
|
||||
"name": "negative"
|
||||
@@ -17911,7 +17911,7 @@
|
||||
},
|
||||
"length": {
|
||||
"name": "length",
|
||||
"tooltip": "ピクセルフレーム数。latent_tを偶数にするため切り捨てられます(patch_size_t=2の要件、例:49 → 45)。"
|
||||
"tooltip": "ピクセルフレーム数。latent_tが偶数になるよう切り捨てられます(patch_size_t=2の要件)、例:49 → 45。"
|
||||
},
|
||||
"optical_flow": {
|
||||
"name": "optical_flow",
|
||||
|
||||
@@ -17911,11 +17911,11 @@
|
||||
},
|
||||
"length": {
|
||||
"name": "length",
|
||||
"tooltip": "픽셀 프레임 수입니다. latent_t가 짝수가 되도록 내림 처리됩니다 (patch_size_t=2 필요), 예: 49 → 45."
|
||||
"tooltip": "픽셀 프레임 수입니다. latent_t를 짝수로 맞추기 위해 내림 처리됩니다 (patch_size_t=2 필요), 예: 49 → 45."
|
||||
},
|
||||
"optical_flow": {
|
||||
"name": "optical_flow",
|
||||
"tooltip": "OpticalFlowLoader에서 가져온 광학 흐름 모델 (RAFT-large)."
|
||||
"tooltip": "OpticalFlowLoader에서 불러온 광학 흐름 모델 (RAFT-large)."
|
||||
},
|
||||
"video": {
|
||||
"name": "video",
|
||||
|
||||
@@ -17937,7 +17937,7 @@
|
||||
"inputs": {
|
||||
"warped_noise": {
|
||||
"name": "warped_noise",
|
||||
"tooltip": "VOIDWarpedNoise'dan bozulmuş gürültü latent"
|
||||
"tooltip": "VOIDWarpedNoise'dan alınan bozulmuş gürültü latent"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
|
||||
@@ -17846,7 +17846,7 @@
|
||||
},
|
||||
"quadmask": {
|
||||
"name": "quadmask",
|
||||
"tooltip": "来自 VOIDQuadmaskPreprocess 的预处理 quadmask [T, H, W]"
|
||||
"tooltip": "来自 VOIDQuadmaskPreprocess 的预处理四边形掩码 [T, H, W]"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
@@ -17879,7 +17879,7 @@
|
||||
"inputs": {
|
||||
"dilate_width": {
|
||||
"name": "dilate_width",
|
||||
"tooltip": "主 mask 区域的膨胀半径(0 = 不膨胀)"
|
||||
"tooltip": "主掩码区域的膨胀半径(0 = 不膨胀)"
|
||||
},
|
||||
"mask": {
|
||||
"name": "mask"
|
||||
@@ -17937,7 +17937,7 @@
|
||||
"inputs": {
|
||||
"warped_noise": {
|
||||
"name": "warped_noise",
|
||||
"tooltip": "来自 VOIDWarpedNoise 的 warped noise latent"
|
||||
"tooltip": "来自 VOIDWarpedNoise 的扭曲噪声 latent"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
|
||||
<template #header>
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
@click.self="focusedAsset = null"
|
||||
>
|
||||
@@ -53,7 +52,6 @@
|
||||
<AssetFilterBar
|
||||
:assets="categoryFilteredAssets"
|
||||
:show-ownership-filter
|
||||
:content-style="selectContentStyle"
|
||||
@filter-change="updateFilters"
|
||||
@click.self="focusedAsset = null"
|
||||
/>
|
||||
@@ -74,12 +72,7 @@
|
||||
</template>
|
||||
|
||||
<template #rightPanel>
|
||||
<ModelInfoPanel
|
||||
v-if="focusedAsset"
|
||||
:asset="focusedAsset"
|
||||
:cache-key
|
||||
:select-content-style="selectContentStyle"
|
||||
/>
|
||||
<ModelInfoPanel v-if="focusedAsset" :asset="focusedAsset" :cache-key />
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full items-center justify-center p-6 text-center wrap-break-word text-muted"
|
||||
@@ -99,7 +92,6 @@ 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'
|
||||
@@ -117,8 +109,6 @@ 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
|
||||
|
||||
@@ -17,6 +17,7 @@ const createAssetData = (
|
||||
{ label: '2.1 GB', type: 'size' }
|
||||
],
|
||||
stats: {
|
||||
formattedDate: '3/15/25',
|
||||
downloadCount: '1.8k',
|
||||
stars: '4.2k'
|
||||
},
|
||||
@@ -146,7 +147,8 @@ export const EdgeCases: Story = {
|
||||
name: 'No Stars',
|
||||
secondaryText: 'Testing missing stars data gracefully',
|
||||
stats: {
|
||||
downloadCount: '1.8k'
|
||||
downloadCount: '1.8k',
|
||||
formattedDate: '3/15/25'
|
||||
}
|
||||
}),
|
||||
// No downloads
|
||||
@@ -155,7 +157,8 @@ export const EdgeCases: Story = {
|
||||
name: 'No Downloads',
|
||||
secondaryText: 'Testing missing downloads data gracefully',
|
||||
stats: {
|
||||
stars: '4.2k'
|
||||
stars: '4.2k',
|
||||
formattedDate: '3/15/25'
|
||||
}
|
||||
}),
|
||||
// No date
|
||||
|
||||
@@ -103,9 +103,12 @@
|
||||
<i class="icon-[lucide--download] size-3" />
|
||||
{{ asset.stats.downloadCount }}
|
||||
</span>
|
||||
<span v-if="formattedDate" class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="asset.stats.formattedDate"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<i class="icon-[lucide--clock] size-3" />
|
||||
{{ formattedDate }}
|
||||
{{ asset.stats.formattedDate }}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -159,7 +162,7 @@ const emit = defineEmits<{
|
||||
showInfo: [asset: AssetDisplayItem]
|
||||
}>()
|
||||
|
||||
const { t, d } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const { closeDialog } = useDialogStore()
|
||||
const { isDownloadedThisSession, acknowledgeAsset } = useAssetDownloadStore()
|
||||
@@ -173,15 +176,6 @@ const descId = useId()
|
||||
|
||||
const displayName = computed(() => getAssetCardTitle(asset))
|
||||
|
||||
// Format at render so locale switches re-flow; the upstream WeakMap caches
|
||||
// AssetItem -> AssetDisplayItem by reference, which would otherwise pin the
|
||||
// formatted string to whichever locale was active when first transformed.
|
||||
const formattedDate = computed(() =>
|
||||
asset.created_at
|
||||
? d(new Date(asset.created_at), { dateStyle: 'short' })
|
||||
: undefined
|
||||
)
|
||||
|
||||
const isNewlyImported = computed(() => isDownloadedThisSession(asset.id))
|
||||
|
||||
const showAssetOptions = computed(() => !(asset.is_immutable ?? true))
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
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"
|
||||
@@ -23,7 +22,6 @@
|
||||
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"
|
||||
@@ -34,7 +32,6 @@
|
||||
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"
|
||||
@@ -46,7 +43,6 @@
|
||||
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"
|
||||
@@ -61,7 +57,6 @@
|
||||
|
||||
<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'
|
||||
@@ -83,14 +78,9 @@ const sortOptions = computed(() => [
|
||||
{ name: t('assetBrowser.sortZA'), value: 'name-desc' as const }
|
||||
])
|
||||
|
||||
const {
|
||||
assets = [],
|
||||
showOwnershipFilter = false,
|
||||
contentStyle
|
||||
} = defineProps<{
|
||||
const { assets = [], showOwnershipFilter = false } = defineProps<{
|
||||
assets?: AssetItem[]
|
||||
showOwnershipFilter?: boolean
|
||||
contentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const selectedFileFormats = ref<SelectOption[]>([])
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex flex-col gap-4 text-sm text-muted-foreground"
|
||||
>
|
||||
<div 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') }}
|
||||
@@ -42,7 +39,6 @@
|
||||
"
|
||||
:options="modelTypes"
|
||||
:disabled="isLoading"
|
||||
:content-style="selectContentStyle"
|
||||
data-attr="upload-model-step2-type-selector"
|
||||
/>
|
||||
</div>
|
||||
@@ -51,7 +47,6 @@
|
||||
|
||||
<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'
|
||||
|
||||
@@ -63,6 +58,4 @@ 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 :style="selectContentStyle">
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="option in modelTypes"
|
||||
:key="option.value"
|
||||
@@ -210,7 +210,6 @@
|
||||
<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'
|
||||
@@ -258,10 +257,9 @@ const accordionClass = cn(
|
||||
'border-t border-border-default bg-modal-panel-background'
|
||||
)
|
||||
|
||||
const { asset, cacheKey, selectContentStyle } = defineProps<{
|
||||
const { asset, cacheKey } = defineProps<{
|
||||
asset: AssetDisplayItem
|
||||
cacheKey?: string
|
||||
selectContentStyle?: StyleValue
|
||||
}>()
|
||||
|
||||
const assetsStore = useAssetsStore()
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import * as assetMetadataUtils from '@/platform/assets/utils/assetMetadataUtils'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string) => key,
|
||||
d: (date: Date) => date.toLocaleDateString()
|
||||
}))
|
||||
|
||||
const ASSET_COUNT = 200
|
||||
const CATEGORIES = ['inputs', 'outputs'] as const
|
||||
const TAB_SWITCHES = 6
|
||||
|
||||
function makeAsset(index: number): AssetItem {
|
||||
const category = CATEGORIES[index % CATEGORIES.length]
|
||||
return {
|
||||
id: `asset-${index}`,
|
||||
name: `asset-${index}.safetensors`,
|
||||
asset_hash: `blake3:${index}`,
|
||||
size: 1024,
|
||||
mime_type: 'application/octet-stream',
|
||||
tags: ['models', category],
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
last_access_time: '2024-01-01T00:00:00Z',
|
||||
is_immutable: false
|
||||
}
|
||||
}
|
||||
|
||||
describe('useAssetBrowser - filter tab switching perf (FE-229)', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('does not re-transform every asset on each filter tab switch', async () => {
|
||||
const assets = Array.from({ length: ASSET_COUNT }, (_, i) => makeAsset(i))
|
||||
const filenameSpy = vi.spyOn(assetMetadataUtils, 'getAssetFilename')
|
||||
|
||||
const { selectedNavItem, filteredAssets } = useAssetBrowser(ref(assets))
|
||||
|
||||
// Initial materialization of the 'all' tab.
|
||||
void filteredAssets.value
|
||||
await nextTick()
|
||||
const baselineCalls = filenameSpy.mock.calls.length
|
||||
|
||||
// Simulate the user clicking back and forth between All / Inputs / Outputs.
|
||||
const tabs: ('all' | 'inputs' | 'outputs')[] = [
|
||||
'inputs',
|
||||
'outputs',
|
||||
'all',
|
||||
'inputs',
|
||||
'outputs',
|
||||
'all'
|
||||
]
|
||||
expect(tabs).toHaveLength(TAB_SWITCHES)
|
||||
|
||||
for (const tab of tabs) {
|
||||
selectedNavItem.value = tab
|
||||
void filteredAssets.value
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const switchCalls = filenameSpy.mock.calls.length - baselineCalls
|
||||
|
||||
// Naive (no memoization) cost is approximately:
|
||||
// inputs (100) + outputs (100) + all (200) + inputs (100) + outputs (100) + all (200) = 800.
|
||||
// With per-asset memoization the same asset object should never be transformed twice,
|
||||
// so total work across all tab switches must stay within a small multiple of ASSET_COUNT.
|
||||
const budget = ASSET_COUNT * 2
|
||||
expect(switchCalls).toBeLessThanOrEqual(budget)
|
||||
})
|
||||
|
||||
it('returns identical display item references for unchanged assets across tab switches', async () => {
|
||||
const assets = Array.from({ length: ASSET_COUNT }, (_, i) => makeAsset(i))
|
||||
|
||||
const { selectedNavItem, filteredAssets } = useAssetBrowser(ref(assets))
|
||||
|
||||
const firstAllSnapshot = new Map(
|
||||
filteredAssets.value.map((item) => [item.id, item])
|
||||
)
|
||||
await nextTick()
|
||||
|
||||
selectedNavItem.value = 'inputs'
|
||||
void filteredAssets.value
|
||||
await nextTick()
|
||||
|
||||
selectedNavItem.value = 'all'
|
||||
const secondAll = filteredAssets.value
|
||||
await nextTick()
|
||||
|
||||
// If transformAssetForDisplay is memoized per asset, the display items for
|
||||
// the unchanged underlying assets should be the very same object identity
|
||||
// when we navigate back to 'all'. Without memoization every re-render
|
||||
// produces brand-new objects, which forces downstream components
|
||||
// (AssetGrid / AssetCard) to re-render every card.
|
||||
const reusedReferences = secondAll.filter(
|
||||
(item) => firstAllSnapshot.get(item.id) === item
|
||||
).length
|
||||
|
||||
expect(reusedReferences).toBe(ASSET_COUNT)
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { useFuse } from '@vueuse/integrations/useFuse'
|
||||
import type { UseFuseOptions } from '@vueuse/integrations/useFuse'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { d, t } from '@/i18n'
|
||||
import type {
|
||||
AssetFilterState,
|
||||
OwnershipOption
|
||||
@@ -38,51 +38,12 @@ export interface AssetDisplayItem extends AssetItem {
|
||||
secondaryText: string
|
||||
badges: AssetBadge[]
|
||||
stats: {
|
||||
formattedDate?: string
|
||||
downloadCount?: string
|
||||
stars?: string
|
||||
}
|
||||
}
|
||||
|
||||
const displayItemCache = new WeakMap<AssetItem, AssetDisplayItem>()
|
||||
|
||||
function buildDisplayItem(asset: AssetItem): AssetDisplayItem {
|
||||
const badges: AssetBadge[] = []
|
||||
|
||||
const typeTag = asset.tags.find((tag) => tag !== 'models')
|
||||
if (typeTag) {
|
||||
const badgeLabel = typeTag.includes('/')
|
||||
? typeTag.substring(typeTag.indexOf('/') + 1)
|
||||
: typeTag
|
||||
|
||||
badges.push({ label: badgeLabel, type: 'type' })
|
||||
}
|
||||
|
||||
for (const model of getAssetBaseModels(asset)) {
|
||||
badges.push({ label: model, type: 'base' })
|
||||
}
|
||||
|
||||
// Intentionally no formatted date here — the WeakMap caches by AssetItem
|
||||
// reference, so a pre-formatted string would pin the locale active at first
|
||||
// transform. AssetCard formats `created_at` at render via `d()` instead.
|
||||
return {
|
||||
...asset,
|
||||
secondaryText: getAssetFilename(asset),
|
||||
badges,
|
||||
stats: {
|
||||
downloadCount: undefined,
|
||||
stars: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transformAssetForDisplay(asset: AssetItem): AssetDisplayItem {
|
||||
const cached = displayItemCache.get(asset)
|
||||
if (cached) return cached
|
||||
const built = buildDisplayItem(asset)
|
||||
displayItemCache.set(asset, built)
|
||||
return built
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Browser composable
|
||||
* Manages search, filtering, asset transformation and selection logic
|
||||
@@ -121,6 +82,46 @@ export function useAssetBrowser(
|
||||
return selectedNavItem.value
|
||||
})
|
||||
|
||||
// Transform API asset to display asset
|
||||
function transformAssetForDisplay(asset: AssetItem): AssetDisplayItem {
|
||||
const secondaryText = getAssetFilename(asset)
|
||||
|
||||
const badges: AssetBadge[] = []
|
||||
|
||||
const typeTag = asset.tags.find((tag) => tag !== 'models')
|
||||
// Type badge from non-root tag
|
||||
if (typeTag) {
|
||||
// Remove category prefix from badge label (e.g. "checkpoint/model" → "model")
|
||||
const badgeLabel = typeTag.includes('/')
|
||||
? typeTag.substring(typeTag.indexOf('/') + 1)
|
||||
: typeTag
|
||||
|
||||
badges.push({ label: badgeLabel, type: 'type' })
|
||||
}
|
||||
|
||||
// Base model badges from metadata
|
||||
const baseModels = getAssetBaseModels(asset)
|
||||
for (const model of baseModels) {
|
||||
badges.push({ label: model, type: 'base' })
|
||||
}
|
||||
|
||||
// Create display stats from API data
|
||||
const stats = {
|
||||
formattedDate: asset.created_at
|
||||
? d(new Date(asset.created_at), { dateStyle: 'short' })
|
||||
: undefined,
|
||||
downloadCount: undefined, // Not available in API
|
||||
stars: undefined // Not available in API
|
||||
}
|
||||
|
||||
return {
|
||||
...asset,
|
||||
secondaryText,
|
||||
badges,
|
||||
stats
|
||||
}
|
||||
}
|
||||
|
||||
const typeCategories = computed<NavItemData[]>(() => {
|
||||
const categories = assets.value
|
||||
.filter((asset) => asset.tags.includes(MODELS_TAG))
|
||||
|
||||
@@ -14,21 +14,16 @@
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<div
|
||||
:ref="primeVueOverlay.overlayScopeRef"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
>
|
||||
<div 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
|
||||
@@ -92,7 +87,6 @@
|
||||
v-model="sortField"
|
||||
:label="$t('g.sort')"
|
||||
:options="availableSortOptions"
|
||||
:content-style="selectContentStyle"
|
||||
class="w-48"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -169,7 +163,6 @@ 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'
|
||||
@@ -204,8 +197,6 @@ 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()
|
||||
|
||||