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:
Christian Byrne
2026-04-06 14:47:38 -07:00
committed by GitHub
parent f04ef94f7c
commit 5cc0eef725
6 changed files with 253 additions and 115 deletions

View File

@@ -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'

View 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>

View 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>

View File

@@ -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,

View File

@@ -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]

View File

@@ -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>