mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
fix: resolve backport typecheck issues on core/1.42 (#10891)
Fixes typecheck errors introduced by recent backports: 1. MultiSelect.vue / SingleSelect.vue (#10397): Replaced with correct Reka UI versions 2. BuilderSaveDialogContent.vue / ViewTypeRadioGroup.vue (#10679): Added missing files 3. confirmDialog.ts (#10679): Added key property to ConfirmDialogOptions 4. App.vue (#10445): Removed unused imports Verified: pnpm typecheck passes.
This commit is contained in:
@@ -12,8 +12,6 @@ import { computed, onMounted, watch } from 'vue'
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { isDesktop } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
71
src/components/builder/BuilderSaveDialogContent.vue
Normal file
71
src/components/builder/BuilderSaveDialogContent.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<BuilderDialog @close="emit('close')">
|
||||
<template #title>
|
||||
{{ $t('builderToolbar.saveAs') }}
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label :for="inputId" class="text-sm text-muted-foreground">
|
||||
{{ $t('builderToolbar.filename') }}
|
||||
</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
v-model="filename"
|
||||
autofocus
|
||||
type="text"
|
||||
class="focus-visible:ring-ring flex h-10 min-h-8 items-center self-stretch rounded-lg border-none bg-secondary-background pl-4 text-sm text-base-foreground"
|
||||
@keydown.enter="
|
||||
filename.trim() && emit('save', filename.trim(), openAsApp)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label :id="radioGroupLabelId" class="text-sm text-muted-foreground">
|
||||
{{ $t('builderToolbar.defaultViewLabel') }}
|
||||
</label>
|
||||
<ViewTypeRadioGroup
|
||||
v-model="openAsApp"
|
||||
:aria-labelledby="radioGroupLabelId"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button variant="muted-textonly" size="lg" @click="emit('close')">
|
||||
{{ $t('g.cancel') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
:disabled="!filename.trim()"
|
||||
@click="emit('save', filename.trim(), openAsApp)"
|
||||
>
|
||||
{{ $t('g.save') }}
|
||||
</Button>
|
||||
</template>
|
||||
</BuilderDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useId } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
import BuilderDialog from './BuilderDialog.vue'
|
||||
import ViewTypeRadioGroup from './ViewTypeRadioGroup.vue'
|
||||
|
||||
const { defaultFilename, defaultOpenAsApp = true } = defineProps<{
|
||||
defaultFilename: string
|
||||
defaultOpenAsApp?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [filename: string, openAsApp: boolean]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const inputId = useId()
|
||||
const radioGroupLabelId = useId()
|
||||
const filename = ref(defaultFilename)
|
||||
const openAsApp = ref(defaultOpenAsApp)
|
||||
</script>
|
||||
74
src/components/builder/ViewTypeRadioGroup.vue
Normal file
74
src/components/builder/ViewTypeRadioGroup.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div role="radiogroup" v-bind="$attrs" :class="cn('flex flex-col', gapClass)">
|
||||
<Button
|
||||
v-for="option in viewTypeOptions"
|
||||
:key="option.value.toString()"
|
||||
role="radio"
|
||||
:aria-checked="modelValue === option.value"
|
||||
:class="
|
||||
cn(
|
||||
'flex cursor-pointer items-center gap-2 self-stretch rounded-lg border-none bg-transparent py-2 pr-4 pl-2 text-base-foreground transition-colors hover:bg-secondary-background',
|
||||
heightClass,
|
||||
modelValue === option.value && 'bg-secondary-background'
|
||||
)
|
||||
"
|
||||
variant="textonly"
|
||||
@click="
|
||||
modelValue !== option.value && emit('update:modelValue', option.value)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex size-8 min-h-8 items-center justify-center rounded-lg bg-secondary-background-hover"
|
||||
>
|
||||
<i :class="cn(option.icon, 'size-4')" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="mx-2 flex flex-1 flex-col items-start">
|
||||
<span class="text-sm font-medium text-base-foreground">
|
||||
{{ option.title }}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ option.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
<i
|
||||
v-if="modelValue === option.value"
|
||||
class="icon-[lucide--check] size-4 text-base-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { size = 'md' } = defineProps<{
|
||||
modelValue: boolean
|
||||
size?: 'sm' | 'md'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const viewTypeOptions = [
|
||||
{
|
||||
value: true,
|
||||
icon: 'icon-[lucide--app-window]',
|
||||
title: t('builderToolbar.app'),
|
||||
subtitle: t('builderToolbar.appDescription')
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
icon: 'icon-[comfy--workflow]',
|
||||
title: t('builderToolbar.nodeGraph'),
|
||||
subtitle: t('builderToolbar.nodeGraphDescription')
|
||||
}
|
||||
]
|
||||
const heightClass = size === 'sm' ? 'h-12' : 'h-14'
|
||||
const gapClass = size === 'sm' ? 'gap-1' : 'gap-2'
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@ import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
interface ConfirmDialogOptions {
|
||||
key?: string
|
||||
headerProps?: ComponentAttrs<typeof ConfirmHeader>
|
||||
props?: ComponentAttrs<typeof ConfirmBody>
|
||||
footerProps?: ComponentAttrs<typeof ConfirmFooter>
|
||||
@@ -12,8 +13,9 @@ interface ConfirmDialogOptions {
|
||||
|
||||
export function showConfirmDialog(options: ConfirmDialogOptions = {}) {
|
||||
const dialogStore = useDialogStore()
|
||||
const { headerProps, props, footerProps } = options
|
||||
const { key, headerProps, props, footerProps } = options
|
||||
return dialogStore.showDialog({
|
||||
key,
|
||||
headerComponent: ConfirmHeader,
|
||||
component: ConfirmBody,
|
||||
footerComponent: ConfirmFooter,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<!--
|
||||
Note: Unlike SingleSelect, we don't need an explicit options prop because:
|
||||
1. Our value template only shows a static label (not dynamic based on selection)
|
||||
2. We display a count badge instead of actual selected labels
|
||||
3. All PrimeVue props (including options) are passed via v-bind="$attrs"
|
||||
option-label="name" is required because our option template directly accesses option.name
|
||||
max-selected-labels="0" is required to show count badge instead of selected item labels
|
||||
-->
|
||||
<MultiSelect
|
||||
<ComboboxRoot
|
||||
v-model="selectedItems"
|
||||
v-model:open="isOpen"
|
||||
multiple
|
||||
@@ -30,27 +22,22 @@
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-if="showSelectedCount || showClearButton"
|
||||
class="mt-2 flex items-center justify-between"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 items-center overflow-hidden py-2 whitespace-nowrap',
|
||||
size === 'md' ? 'pl-3' : 'pl-4'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="showSelectedCount"
|
||||
class="px-1 text-sm text-base-foreground"
|
||||
>
|
||||
{{
|
||||
selectedCount > 0
|
||||
? $t('g.itemsSelected', { selectedCount })
|
||||
: $t('g.itemSelected', { selectedCount })
|
||||
}}
|
||||
<span :class="size === 'md' ? 'text-xs' : 'text-sm'">
|
||||
{{ label }}
|
||||
</span>
|
||||
<Button
|
||||
v-if="showClearButton"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click.stop="selectedItems = []"
|
||||
<span
|
||||
v-if="selectedCount > 0"
|
||||
class="pointer-events-none absolute -top-2 -right-2 z-10 flex size-5 items-center justify-center rounded-full bg-base-foreground text-xs font-semibold text-base-background"
|
||||
>
|
||||
{{ $t('g.clearAll') }}
|
||||
</Button>
|
||||
{{ selectedCount }}
|
||||
</span>
|
||||
</div>
|
||||
<div :class="selectDropdownClass">
|
||||
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
|
||||
@@ -58,41 +45,69 @@
|
||||
</ComboboxTrigger>
|
||||
</ComboboxAnchor>
|
||||
|
||||
<!-- Trigger value (keep text scale identical) -->
|
||||
<template #value>
|
||||
<span :class="size === 'md' ? 'text-xs' : 'text-sm'">
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="selectedCount > 0"
|
||||
class="pointer-events-none absolute -top-2 -right-2 z-10 flex size-5 items-center justify-center rounded-full bg-base-foreground text-xs font-semibold text-base-background"
|
||||
>
|
||||
{{ selectedCount }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Chevron size identical to current -->
|
||||
<template #dropdownicon>
|
||||
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
|
||||
</template>
|
||||
|
||||
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
||||
<template #option="slotProps">
|
||||
<div
|
||||
role="button"
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
<ComboboxPortal>
|
||||
<ComboboxContent
|
||||
position="popper"
|
||||
:side-offset="8"
|
||||
align="start"
|
||||
:style="popoverStyle"
|
||||
:class="selectContentClass"
|
||||
@keydown="onContentKeydown"
|
||||
@focus-outside="preventFocusDismiss"
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded-sm p-0.5 transition-all duration-200"
|
||||
v-if="showSearchBox || showSelectedCount || showClearButton"
|
||||
class="flex flex-col px-2 pt-2 pb-0"
|
||||
>
|
||||
<div
|
||||
v-if="showSearchBox"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-2 rounded-lg border border-solid border-border-default px-3 py-1.5',
|
||||
(showSelectedCount || showClearButton) && 'mb-2'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--search] shrink-0 text-sm text-muted-foreground"
|
||||
/>
|
||||
<ComboboxInput
|
||||
v-model="searchQuery"
|
||||
:placeholder="searchPlaceholder ?? t('g.search')"
|
||||
class="w-full border-none bg-transparent text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showSelectedCount || showClearButton"
|
||||
class="mt-2 flex items-center justify-between"
|
||||
>
|
||||
<span
|
||||
v-if="showSelectedCount"
|
||||
class="px-1 text-sm text-base-foreground"
|
||||
>
|
||||
{{ $t('g.itemsSelected', { count: selectedCount }) }}
|
||||
</span>
|
||||
<Button
|
||||
v-if="showClearButton"
|
||||
variant="textonly"
|
||||
size="md"
|
||||
@click.stop="selectedItems = []"
|
||||
>
|
||||
{{ $t('g.clearAll') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="my-4 h-px bg-border-default" />
|
||||
</div>
|
||||
|
||||
<ComboboxViewport
|
||||
:class="
|
||||
slotProps.selected
|
||||
? 'bg-primary-background'
|
||||
: 'bg-secondary-background'
|
||||
cn(
|
||||
'flex flex-col gap-0 p-0 text-sm',
|
||||
'scrollbar-custom overflow-y-auto',
|
||||
'min-w-(--reka-combobox-trigger-width)'
|
||||
)
|
||||
"
|
||||
:style="{ maxHeight: `min(${listMaxHeight}, 50vh)` }"
|
||||
>
|
||||
<ComboboxItem
|
||||
v-for="opt in filteredOptions"
|
||||
@@ -139,7 +154,6 @@ import {
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchInput from '@/components/ui/search-input/SearchInput.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { usePopoverSizing } from '@/composables/usePopoverSizing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -154,17 +168,31 @@ import {
|
||||
} from './select.variants'
|
||||
import type { SelectOption } from './types'
|
||||
|
||||
type Option = SelectOption
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
interface Props {
|
||||
const {
|
||||
label,
|
||||
options = [],
|
||||
size = 'lg',
|
||||
disabled = false,
|
||||
showSearchBox = false,
|
||||
showSelectedCount = false,
|
||||
showClearButton = false,
|
||||
searchPlaceholder,
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth
|
||||
} = defineProps<{
|
||||
/** Input label shown on the trigger button */
|
||||
label?: string
|
||||
/** Available options */
|
||||
options?: SelectOption[]
|
||||
/** Trigger size: 'lg' (40px, Interface) or 'md' (32px, Node) */
|
||||
size?: 'lg' | 'md'
|
||||
/** Disable the select */
|
||||
disabled?: boolean
|
||||
/** Show search box in the panel header */
|
||||
showSearchBox?: boolean
|
||||
/** Show selected count text in the panel header */
|
||||
@@ -179,22 +207,9 @@ interface Props {
|
||||
popoverMinWidth?: string
|
||||
/** Maximum width of the popover (default: auto) */
|
||||
popoverMaxWidth?: string
|
||||
// Note: options prop is intentionally omitted.
|
||||
// It's passed via $attrs to maximize PrimeVue API compatibility
|
||||
}
|
||||
const {
|
||||
label,
|
||||
size = 'lg',
|
||||
showSearchBox = false,
|
||||
showSelectedCount = false,
|
||||
showClearButton = false,
|
||||
searchPlaceholder = 'Search...',
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth
|
||||
} = defineProps<Props>()
|
||||
}>()
|
||||
|
||||
const selectedItems = defineModel<Option[]>({
|
||||
const selectedItems = defineModel<SelectOption[]>({
|
||||
required: true
|
||||
})
|
||||
const searchQuery = defineModel<string>('searchQuery', { default: '' })
|
||||
@@ -218,11 +233,8 @@ const popoverStyle = usePopoverSizing({
|
||||
minWidth: popoverMinWidth,
|
||||
maxWidth: popoverMaxWidth
|
||||
})
|
||||
const attrs = useAttrs()
|
||||
const originalOptions = computed(() => (attrs.options as Option[]) || [])
|
||||
|
||||
// Use VueUse's useFuse for better reactivity and performance
|
||||
const fuseOptions: UseFuseOptions<Option> = {
|
||||
const fuseOptions: UseFuseOptions<SelectOption> = {
|
||||
fuseOptions: {
|
||||
keys: ['name', 'value'],
|
||||
threshold: 0.3,
|
||||
@@ -231,23 +243,20 @@ const fuseOptions: UseFuseOptions<Option> = {
|
||||
matchAllWhenSearchEmpty: true
|
||||
}
|
||||
|
||||
const { results } = useFuse(searchQuery, originalOptions, fuseOptions)
|
||||
const { results } = useFuse(searchQuery, () => options, fuseOptions)
|
||||
|
||||
// Filter options based on search, but always include selected items
|
||||
const filteredOptions = computed(() => {
|
||||
if (!searchQuery.value || searchQuery.value.trim() === '') {
|
||||
return originalOptions.value
|
||||
return options
|
||||
}
|
||||
|
||||
// results.value already contains the search results from useFuse
|
||||
const searchResults = results.value.map(
|
||||
(result: { item: Option }) => result.item
|
||||
(result: { item: SelectOption }) => result.item
|
||||
)
|
||||
|
||||
// Include selected items that aren't in search results
|
||||
const selectedButNotInResults = selectedItems.value.filter(
|
||||
(item) =>
|
||||
!searchResults.some((result: Option) => result.value === item.value)
|
||||
!searchResults.some((result: SelectOption) => result.value === item.value)
|
||||
)
|
||||
|
||||
return [...selectedButNotInResults, ...searchResults]
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn('flex items-center gap-2', size === 'md' ? 'text-xs' : 'text-sm')
|
||||
cn(
|
||||
'flex flex-1 items-center gap-2 overflow-hidden py-2',
|
||||
size === 'md' ? 'pl-3 text-xs' : 'pl-4 text-sm'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="loading"
|
||||
class="icon-[lucide--loader-circle] animate-spin text-muted-foreground"
|
||||
class="icon-[lucide--loader-circle] shrink-0 animate-spin text-muted-foreground"
|
||||
/>
|
||||
<slot v-else name="icon" />
|
||||
<SelectValue :placeholder="label" class="truncate" />
|
||||
@@ -81,6 +84,7 @@ import {
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { usePopoverSizing } from '@/composables/usePopoverSizing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import {
|
||||
@@ -102,16 +106,12 @@ const {
|
||||
size = 'lg',
|
||||
invalid = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
listMaxHeight = '28rem',
|
||||
popoverMinWidth,
|
||||
popoverMaxWidth
|
||||
} = defineProps<{
|
||||
label?: string
|
||||
/**
|
||||
* Required for displaying the selected item's label.
|
||||
* Cannot rely on $attrs alone because we need to access options
|
||||
* in getLabel() to map values to their display names.
|
||||
*/
|
||||
options?: SelectOption[]
|
||||
/** Trigger size: 'lg' (40px, Interface) or 'md' (32px, Node) */
|
||||
size?: 'lg' | 'md'
|
||||
@@ -119,6 +119,8 @@ const {
|
||||
invalid?: boolean
|
||||
/** Show loading spinner instead of chevron */
|
||||
loading?: boolean
|
||||
/** Disable the select */
|
||||
disabled?: boolean
|
||||
/** Maximum height of the dropdown panel (default: 28rem) */
|
||||
listMaxHeight?: string
|
||||
/** Minimum width of the popover (default: auto) */
|
||||
@@ -139,26 +141,8 @@ function onContentKeydown(event: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a value to its display label.
|
||||
* Necessary because PrimeVue's value slot doesn't provide the selected item's label,
|
||||
* only the raw value. We need this to show the correct text when an item is selected.
|
||||
*/
|
||||
const getLabel = (val: string | null | undefined) => {
|
||||
if (val == null) return label ?? ''
|
||||
if (!options) return label ?? ''
|
||||
const found = options.find((o) => o.value === val)
|
||||
return found ? found.name : (label ?? '')
|
||||
}
|
||||
|
||||
// Extract complex style logic from template
|
||||
const optionStyle = computed(() => {
|
||||
if (!popoverMinWidth && !popoverMaxWidth) return undefined
|
||||
|
||||
const styles: string[] = []
|
||||
if (popoverMinWidth) styles.push(`min-width: ${popoverMinWidth}`)
|
||||
if (popoverMaxWidth) styles.push(`max-width: ${popoverMaxWidth}`)
|
||||
|
||||
return styles.join('; ')
|
||||
const optionStyle = usePopoverSizing({
|
||||
minWidth: popoverMinWidth,
|
||||
maxWidth: popoverMaxWidth
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user