refactor: replace withDefaults with Vue 3.5 props destructuring (#9150)

## Summary
- Replace all `withDefaults(defineProps<...>())` with Vue 3.5 reactive
props destructuring across 14 components
- Update `props.xxx` references to use destructured variables directly
in script and template

- Fixes #2334

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9150-refactor-replace-withDefaults-with-Vue-3-5-props-destructuring-3116d73d365081e7a721db3369600671)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2026-02-24 05:30:44 +01:00
committed by GitHub
parent be70f6c1e6
commit 724827d8cc
14 changed files with 170 additions and 213 deletions

View File

@@ -7,7 +7,7 @@
option-value="value"
:disabled="isSwitching"
:pt="dropdownPt"
:size="props.size"
:size="size"
class="language-selector"
@change="onLocaleChange"
>
@@ -36,16 +36,10 @@ import { i18n, loadLocale, st } from '@/i18n'
type VariantKey = 'dark' | 'light'
type SizeKey = 'small' | 'large'
const props = withDefaults(
defineProps<{
variant?: VariantKey
size?: SizeKey
}>(),
{
variant: 'dark',
size: 'small'
}
)
const { variant = 'dark', size = 'small' } = defineProps<{
variant?: VariantKey
size?: SizeKey
}>()
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
@@ -104,10 +98,8 @@ const VARIANT_PRESETS = {
const selectedLocale = ref<string>(i18n.global.locale.value)
const isSwitching = ref(false)
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
const variantPreset = computed(
() => VARIANT_PRESETS[props.variant as VariantKey]
)
const sizePreset = computed(() => SIZE_PRESETS[size])
const variantPreset = computed(() => VARIANT_PRESETS[variant])
const dropdownPt = computed(() => ({
root: {

View File

@@ -78,9 +78,7 @@ interface Props {
isActive?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
})
const { item, isActive = false } = defineProps<Props>()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
@@ -103,7 +101,7 @@ const rename = async (
) => {
if (newName && newName !== initialName) {
// Synchronize the node titles with the new name
props.item.updateTitle?.(newName)
item.updateTitle?.(newName)
if (workflowStore.activeSubgraph) {
workflowStore.activeSubgraph.name = newName
@@ -127,13 +125,13 @@ const rename = async (
}
}
const isRoot = props.item.key === 'root'
const isRoot = item.key === 'root'
const tooltipText = computed(() => {
if (hasMissingNodes.value && isRoot) {
return t('breadcrumbsMenu.missingNodesWarning')
}
return props.item.label
return item.label
})
const startRename = async () => {
@@ -145,7 +143,7 @@ const startRename = async () => {
}
isEditing.value = true
itemLabel.value = props.item.label as string
itemLabel.value = item.label as string
void nextTick(() => {
if (itemInputRef.value?.$el) {
itemInputRef.value.$el.focus()
@@ -165,12 +163,12 @@ const handleClick = (event: MouseEvent) => {
}
if (event.detail === 1) {
if (props.isActive) {
if (isActive) {
menu.value?.toggle(event)
} else {
props.item.command?.({ item: props.item, originalEvent: event })
item.command?.({ item: item, originalEvent: event })
}
} else if (props.isActive && event.detail === 2) {
} else if (isActive && event.detail === 2) {
menu.value?.hide()
event.stopPropagation()
event.preventDefault()
@@ -180,7 +178,7 @@ const handleClick = (event: MouseEvent) => {
const inputBlur = async (doRename: boolean) => {
if (doRename) {
await rename(itemLabel.value, props.item.label as string)
await rename(itemLabel.value, item.label as string)
}
isEditing.value = false

View File

@@ -24,9 +24,7 @@ interface Props {
modelValue: number
}
withDefaults(defineProps<Props>(), {
step: 1
})
const { label, min, max, step = 1, modelValue } = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: number]

View File

@@ -75,15 +75,10 @@ import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
type OverlayState = 'hidden' | 'active' | 'expanded'
const props = withDefaults(
defineProps<{
expanded?: boolean
menuHovered?: boolean
}>(),
{
menuHovered: false
}
)
const { expanded, menuHovered = false } = defineProps<{
expanded?: boolean
menuHovered?: boolean
}>()
const emit = defineEmits<{
(e: 'update:expanded', value: boolean): void
@@ -106,13 +101,12 @@ const {
currentNodeProgressStyle
} = useQueueProgress()
const isHovered = ref(false)
const isOverlayHovered = computed(() => isHovered.value || props.menuHovered)
const isOverlayHovered = computed(() => isHovered.value || menuHovered)
const internalExpanded = ref(false)
const isExpanded = computed({
get: () =>
props.expanded === undefined ? internalExpanded.value : props.expanded,
get: () => (expanded === undefined ? internalExpanded.value : expanded),
set: (value) => {
if (props.expanded === undefined) {
if (expanded === undefined) {
internalExpanded.value = value
}
emit('update:expanded', value)

View File

@@ -17,10 +17,7 @@
@mouseenter="onPopoverEnter"
@mouseleave="onPopoverLeave"
>
<JobDetailsPopover
:job-id="props.jobId"
:workflow-id="props.workflowId"
/>
<JobDetailsPopover :job-id="jobId" :workflow-id="workflowId" />
</div>
</Teleport>
<Teleport to="body">
@@ -36,7 +33,7 @@
>
<QueueAssetPreview
:image-url="iconImageUrl!"
:name="props.title"
:name="title"
:time-label="rightText || undefined"
@image-click="emit('view')"
/>
@@ -49,23 +46,20 @@
>
<div
v-if="
props.state === 'running' &&
hasAnyProgressPercent(
props.progressTotalPercent,
props.progressCurrentPercent
)
state === 'running' &&
hasAnyProgressPercent(progressTotalPercent, progressCurrentPercent)
"
:class="progressBarContainerClass"
>
<div
v-if="hasProgressPercent(props.progressTotalPercent)"
v-if="hasProgressPercent(progressTotalPercent)"
:class="progressBarPrimaryClass"
:style="progressPercentStyle(props.progressTotalPercent)"
:style="progressPercentStyle(progressTotalPercent)"
/>
<div
v-if="hasProgressPercent(props.progressCurrentPercent)"
v-if="hasProgressPercent(progressCurrentPercent)"
:class="progressBarSecondaryClass"
:style="progressPercentStyle(props.progressCurrentPercent)"
:style="progressPercentStyle(progressCurrentPercent)"
/>
</div>
@@ -93,8 +87,8 @@
</div>
<div class="relative z-1 min-w-0 flex-1">
<div class="truncate opacity-90" :title="props.title">
<slot name="primary">{{ props.title }}</slot>
<div class="truncate opacity-90" :title="title">
<slot name="primary">{{ title }}</slot>
</div>
</div>
@@ -131,7 +125,7 @@
class="inline-flex items-center gap-2 pr-1"
>
<Button
v-if="props.state === 'failed' && computedShowClear"
v-if="state === 'failed' && computedShowClear"
v-tooltip.top="deleteTooltipConfig"
variant="destructive"
size="icon"
@@ -142,8 +136,8 @@
</Button>
<Button
v-else-if="
props.state !== 'completed' &&
props.state !== 'running' &&
state !== 'completed' &&
state !== 'running' &&
computedShowClear
"
v-tooltip.top="cancelTooltipConfig"
@@ -155,14 +149,14 @@
<i class="icon-[lucide--x] size-4" />
</Button>
<Button
v-else-if="props.state === 'completed'"
v-else-if="state === 'completed'"
variant="textonly"
size="sm"
@click.stop="emit('view')"
>{{ t('menuLabels.View') }}</Button
>
<Button
v-if="props.showMenu !== undefined ? props.showMenu : true"
v-if="showMenu !== undefined ? showMenu : true"
v-tooltip.top="moreTooltipConfig"
variant="textonly"
size="icon-sm"
@@ -172,17 +166,13 @@
<i class="icon-[lucide--more-horizontal] size-4" />
</Button>
</div>
<div
v-else-if="props.state !== 'running'"
key="secondary"
class="pr-2"
>
<slot name="secondary">{{ props.rightText }}</slot>
<div v-else-if="state !== 'running'" key="secondary" class="pr-2">
<slot name="secondary">{{ rightText }}</slot>
</div>
</Transition>
<!-- Running job cancel button - always visible -->
<Button
v-if="props.state === 'running' && computedShowClear"
v-if="state === 'running' && computedShowClear"
v-tooltip.top="cancelTooltipConfig"
variant="destructive"
size="icon"
@@ -209,34 +199,33 @@ import type { JobState } from '@/types/queue'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
const props = withDefaults(
defineProps<{
jobId: string
workflowId?: string
state: JobState
title: string
rightText?: string
iconName?: string
iconImageUrl?: string
showClear?: boolean
showMenu?: boolean
progressTotalPercent?: number
progressCurrentPercent?: number
activeDetailsId?: string | null
}>(),
{
workflowId: undefined,
rightText: '',
iconName: undefined,
iconImageUrl: undefined,
showClear: undefined,
showMenu: undefined,
progressTotalPercent: undefined,
progressCurrentPercent: undefined,
runningNodeName: undefined,
activeDetailsId: null
}
)
const {
jobId,
workflowId,
state,
title,
rightText = '',
iconName,
iconImageUrl,
showClear,
showMenu,
progressTotalPercent,
progressCurrentPercent,
activeDetailsId = null
} = defineProps<{
jobId: string
workflowId?: string
state: JobState
title: string
rightText?: string
iconName?: string
iconImageUrl?: string
showClear?: boolean
showMenu?: boolean
progressTotalPercent?: number
progressCurrentPercent?: number
activeDetailsId?: string | null
}>()
const emit = defineEmits<{
(e: 'cancel'): void
@@ -262,14 +251,14 @@ const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
const rowRef = ref<HTMLDivElement | null>(null)
const showDetails = computed(() => props.activeDetailsId === props.jobId)
const showDetails = computed(() => activeDetailsId === jobId)
const onRowEnter = () => {
if (!isPreviewVisible.value) emit('details-enter', props.jobId)
if (!isPreviewVisible.value) emit('details-enter', jobId)
}
const onRowLeave = () => emit('details-leave', props.jobId)
const onPopoverEnter = () => emit('details-enter', props.jobId)
const onPopoverLeave = () => emit('details-leave', props.jobId)
const onRowLeave = () => emit('details-leave', jobId)
const onPopoverEnter = () => emit('details-enter', jobId)
const onPopoverLeave = () => emit('details-leave', jobId)
const isPreviewVisible = ref(false)
const previewHideTimer = ref<number | null>(null)
@@ -286,9 +275,7 @@ const clearPreviewShowTimer = () => {
previewShowTimer.value = null
}
}
const canShowPreview = computed(
() => props.state === 'completed' && !!props.iconImageUrl
)
const canShowPreview = computed(() => state === 'completed' && !!iconImageUrl)
const scheduleShowPreview = () => {
if (!canShowPreview.value) return
clearPreviewHideTimer()
@@ -343,23 +330,23 @@ watch(
const isHovered = ref(false)
const iconClass = computed(() => {
if (props.iconName) return props.iconName
return iconForJobState(props.state)
if (iconName) return iconName
return iconForJobState(state)
})
const shouldSpin = computed(
() =>
props.state === 'pending' &&
state === 'pending' &&
iconClass.value === iconForJobState('pending') &&
!props.iconImageUrl
!iconImageUrl
)
const computedShowClear = computed(() => {
if (props.showClear !== undefined) return props.showClear
return props.state !== 'completed'
if (showClear !== undefined) return showClear
return state !== 'completed'
})
const emitDetailsLeave = () => emit('details-leave', props.jobId)
const emitDetailsLeave = () => emit('details-leave', jobId)
const onCancelClick = () => {
emitDetailsLeave()
@@ -372,7 +359,7 @@ const onDeleteClick = () => {
}
const onContextMenu = (event: MouseEvent) => {
const shouldShowMenu = props.showMenu !== undefined ? props.showMenu : true
const shouldShowMenu = showMenu !== undefined ? showMenu : true
if (shouldShowMenu) emit('menu', event)
}
</script>

View File

@@ -11,21 +11,22 @@ interface Props {
disable?: boolean
}
const props = withDefaults(defineProps<Props>(), {
duration: 150,
easingEnter: 'ease-in-out',
easingLeave: 'ease-in-out',
opacityClosed: 0,
opacityOpened: 1
})
const {
duration = 150,
easingEnter = 'ease-in-out',
easingLeave = 'ease-in-out',
opacityClosed = 0,
opacityOpened = 1,
disable
} = defineProps<Props>()
const closed = '0px'
const isMounted = ref(false)
onMounted(() => (isMounted.value = true))
const duration = computed(() =>
isMounted.value && !props.disable ? props.duration : 0
const animationDuration = computed(() =>
isMounted.value && !disable ? duration : 0
)
interface initialStyle {
@@ -95,7 +96,7 @@ function getEnterKeyframes(height: string, initialStyle: initialStyle) {
return [
{
height: closed,
opacity: props.opacityClosed,
opacity: opacityClosed,
paddingTop: closed,
paddingBottom: closed,
borderTopWidth: closed,
@@ -105,7 +106,7 @@ function getEnterKeyframes(height: string, initialStyle: initialStyle) {
},
{
height,
opacity: props.opacityOpened,
opacity: opacityOpened,
paddingTop: initialStyle.paddingTop,
paddingBottom: initialStyle.paddingBottom,
borderTopWidth: initialStyle.borderTopWidth,
@@ -121,7 +122,7 @@ function enterTransition(element: Element, done: () => void) {
const initialStyle = getElementStyle(HTMLElement)
const height = prepareElement(HTMLElement, initialStyle)
const keyframes = getEnterKeyframes(height, initialStyle)
const options = { duration: duration.value, easing: props.easingEnter }
const options = { duration: animationDuration.value, easing: easingEnter }
animateTransition(HTMLElement, initialStyle, done, keyframes, options)
}
@@ -132,7 +133,7 @@ function leaveTransition(element: Element, done: () => void) {
HTMLElement.style.height = height
HTMLElement.style.overflow = 'hidden'
const keyframes = getEnterKeyframes(height, initialStyle).reverse()
const options = { duration: duration.value, easing: props.easingLeave }
const options = { duration: animationDuration.value, easing: easingLeave }
animateTransition(HTMLElement, initialStyle, done, keyframes, options)
}
</script>

View File

@@ -16,20 +16,17 @@ import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
import TopbarBadge from './TopbarBadge.vue'
withDefaults(
defineProps<{
displayMode?: 'full' | 'compact' | 'icon-only'
reverseOrder?: boolean
noPadding?: boolean
backgroundColor?: string
}>(),
{
displayMode: 'full',
reverseOrder: false,
noPadding: false,
backgroundColor: 'var(--comfy-menu-bg)'
}
)
const {
displayMode = 'full',
reverseOrder = false,
noPadding = false,
backgroundColor = 'var(--comfy-menu-bg)'
} = defineProps<{
displayMode?: 'full' | 'compact' | 'icon-only'
reverseOrder?: boolean
noPadding?: boolean
backgroundColor?: string
}>()
const { t } = useI18n()

View File

@@ -129,21 +129,19 @@ import { computed, ref } from 'vue'
import type { TopbarBadge } from '@/types/comfy'
import { cn } from '@/utils/tailwindUtil'
const props = withDefaults(
defineProps<{
badge: TopbarBadge
displayMode?: 'full' | 'compact' | 'icon-only'
reverseOrder?: boolean
noPadding?: boolean
backgroundColor?: string
}>(),
{
displayMode: 'full',
reverseOrder: false,
noPadding: false,
backgroundColor: 'var(--comfy-menu-bg)'
}
)
const {
badge,
displayMode = 'full',
reverseOrder = false,
noPadding = false,
backgroundColor = 'var(--comfy-menu-bg)'
} = defineProps<{
badge: TopbarBadge
displayMode?: 'full' | 'compact' | 'icon-only'
reverseOrder?: boolean
noPadding?: boolean
backgroundColor?: string
}>()
const popover = ref<InstanceType<typeof Popover>>()
@@ -151,10 +149,10 @@ const togglePopover = (event: Event) => {
popover.value?.toggle(event)
}
const variant = computed(() => props.badge.variant ?? 'info')
const variant = computed(() => badge.variant ?? 'info')
const menuBackgroundStyle = computed(() => ({
backgroundColor: props.backgroundColor
backgroundColor: backgroundColor
}))
const labelClasses = computed(() => {
@@ -184,8 +182,8 @@ const textClasses = computed(() => {
const iconColorClass = computed(() => textClasses.value)
const iconClass = computed(() => {
if (props.badge.icon) {
return props.badge.icon
if (badge.icon) {
return badge.icon
}
switch (variant.value) {
case 'error':

View File

@@ -19,16 +19,10 @@ import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'
import TopbarBadge from './TopbarBadge.vue'
withDefaults(
defineProps<{
reverseOrder?: boolean
noPadding?: boolean
}>(),
{
reverseOrder: false,
noPadding: false
}
)
const { reverseOrder = false, noPadding = false } = defineProps<{
reverseOrder?: boolean
noPadding?: boolean
}>()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isXl = breakpoints.greaterOrEqual('xl')

View File

@@ -196,7 +196,7 @@
<Button
:variant="getButtonSeverity(tier)"
:disabled="isButtonDisabled(tier)"
:loading="props.loadingTier === tier.key"
:loading="loadingTier === tier.key"
:class="
cn(
'h-10 w-full',
@@ -305,10 +305,7 @@ interface Props {
loadingTier?: CheckoutTierKey | null
}
const props = withDefaults(defineProps<Props>(), {
isLoading: false,
loadingTier: null
})
const { isLoading = false, loadingTier = null } = defineProps<Props>()
const emit = defineEmits<{
subscribe: [payload: { tierKey: CheckoutTierKey; billingCycle: BillingCycle }]
@@ -463,7 +460,7 @@ const getButtonSeverity = (
}
const isButtonDisabled = (tier: PricingTierConfig): boolean => {
if (props.isLoading) return true
if (isLoading) return true
if (isCurrentPlan(tier.key)) {
// Allow clicking current plan button when cancelled (for resubscribe)
return !isCancelled.value
@@ -495,7 +492,7 @@ const getMonthlyCreditsPerMember = (tier: PricingTierConfig): number =>
tier.pricing.credits
function handleSubscribe(tierKey: CheckoutTierKey) {
if (props.isLoading) return
if (isLoading) return
// Handle resubscribe for cancelled subscription on current plan
if (isCurrentPlan(tierKey)) {

View File

@@ -161,15 +161,10 @@ import { formatTime } from '../../utils/audioUtils'
const { t } = useI18n()
const toast = useToast()
const props = withDefaults(
defineProps<{
hideWhenEmpty?: boolean
showOptionsButton?: boolean
}>(),
{
hideWhenEmpty: true
}
)
const { hideWhenEmpty = true, showOptionsButton } = defineProps<{
hideWhenEmpty?: boolean
showOptionsButton?: boolean
}>()
// Refs
const audioRef = useTemplateRef('audioRef')

View File

@@ -58,11 +58,13 @@ interface Emits {
'update:modelValue': [value: ModelValue]
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
optionLabel: 'label',
optionValue: 'value'
})
const {
modelValue,
options,
optionLabel = 'label',
optionValue = 'value',
disabled = false
} = defineProps<Props>()
const emit = defineEmits<Emits>()
@@ -72,7 +74,7 @@ const getOptionValue = (option: T, index: number): ModelValue => {
return option as ModelValue
}
const valueField = props.optionValue
const valueField = optionValue
const optionRecord = option as Record<string, unknown>
const value =
optionRecord[valueField] ??
@@ -86,7 +88,7 @@ const getOptionValue = (option: T, index: number): ModelValue => {
// for display with PrimeVue compatibility
const getOptionLabel = (option: T): string => {
if (typeof option === 'object' && option !== null) {
const labelField = props.optionLabel
const labelField = optionLabel
const optionRecord = option as Record<string, unknown>
return String(
optionRecord[labelField] ??
@@ -100,14 +102,14 @@ const getOptionLabel = (option: T): string => {
}
const isSelected = (index: number): boolean => {
const optionValue = getOptionValue(props.options[index], index)
return String(optionValue) === String(props.modelValue ?? '')
const optVal = getOptionValue(options[index], index)
return String(optVal) === String(modelValue ?? '')
}
const handleSelect = (index: number) => {
if (props.disabled) return
if (disabled) return
const optionValue = getOptionValue(props.options[index], index)
emit('update:modelValue', optionValue)
const optVal = getOptionValue(options[index], index)
emit('update:modelValue', optVal)
}
</script>

View File

@@ -19,10 +19,17 @@ interface Props {
accept?: string
}
const props = withDefaults(defineProps<Props>(), {
isOpen: false,
placeholder: 'Select...'
})
const {
isOpen = false,
placeholder = 'Select...',
items,
displayItems,
selected,
maxSelectable,
uploadable,
disabled,
accept
} = defineProps<Props>()
const emit = defineEmits<{
(e: 'select-click', event: MouseEvent): void
@@ -30,14 +37,14 @@ const emit = defineEmits<{
}>()
const selectedItems = computed(() => {
const itemsToSearch = props.displayItems ?? props.items
return itemsToSearch.filter((item) => props.selected.has(item.id))
const itemsToSearch = displayItems ?? items
return itemsToSearch.filter((item) => selected.has(item.id))
})
const theButtonStyle = computed(() =>
cn(
'border-0 bg-component-node-widget-background outline-none text-text-secondary',
props.disabled
disabled
? 'cursor-not-allowed'
: 'hover:bg-component-node-widget-background-hovered cursor-pointer',
selectedItems.value.length > 0 && 'text-text-primary'

View File

@@ -11,12 +11,12 @@
{{ $t('manager.conflicts.conflictInfoTitle') }}
</Button>
<Button
v-if="props.buttonText"
v-if="buttonText"
variant="secondary"
size="sm"
@click="handleButtonClick"
>
{{ props.buttonText }}
{{ buttonText }}
</Button>
</div>
</div>
@@ -31,10 +31,7 @@ interface Props {
buttonText?: string
onButtonClick?: () => void
}
const props = withDefaults(defineProps<Props>(), {
buttonText: undefined,
onButtonClick: undefined
})
const { buttonText, onButtonClick } = defineProps<Props>()
const { buildDocsUrl } = useExternalLink()
const dialogStore = useDialogStore()
const handleConflictInfoClick = () => {
@@ -49,8 +46,8 @@ const handleButtonClick = () => {
// Close the conflict dialog
dialogStore.closeDialog({ key: 'global-node-conflict' })
// Execute the custom button action if provided
if (props.onButtonClick) {
props.onButtonClick()
if (onButtonClick) {
onButtonClick()
}
}
</script>