Compare commits
2 Commits
cloud/v1.3
...
improve-er
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71fcd283b3 | ||
|
|
c25a63adc5 |
@@ -51,6 +51,8 @@ defineProps<{
|
||||
canProceed: boolean
|
||||
/** Whether the location step should be disabled */
|
||||
disableLocationStep: boolean
|
||||
/** Whether the migration step should be disabled */
|
||||
disableMigrationStep: boolean
|
||||
/** Whether the settings step should be disabled */
|
||||
disableSettingsStep: boolean
|
||||
}>()
|
||||
|
||||
@@ -160,7 +160,7 @@ export class VueNodeHelpers {
|
||||
return {
|
||||
input: widget.locator('input'),
|
||||
incrementButton: widget.locator('button').first(),
|
||||
decrementButton: widget.locator('button').nth(1)
|
||||
decrementButton: widget.locator('button').last()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 111 KiB |
@@ -15,9 +15,7 @@ test.describe('Vue Integer Widget', () => {
|
||||
await comfyPage.loadWorkflow('vueNodes/linked-int-widget')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
const seedWidget = comfyPage.vueNodes
|
||||
.getWidgetByName('KSampler', 'seed')
|
||||
.first()
|
||||
const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed')
|
||||
const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget)
|
||||
const initialValue = Number(await controls.input.inputValue())
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
<title>ComfyUI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="user.css" />
|
||||
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
|
||||
|
||||
<!-- Fullscreen mode on mobile browsers -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.35.7",
|
||||
"version": "1.35.5",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -264,7 +264,7 @@ if (!releaseInfo) {
|
||||
}
|
||||
|
||||
// Output as JSON for GitHub Actions
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(JSON.stringify(releaseInfo, null, 2))
|
||||
|
||||
export { resolveRelease }
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
import { isStaging } from '@/config/staging'
|
||||
// Global variable from vite build defined in global.d.ts
|
||||
// eslint-disable-next-line no-undef
|
||||
const isStaging = !__USE_PROD_CONFIG__
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<header
|
||||
class="flex items-center justify-between border-b border-interface-stroke px-4 py-4"
|
||||
>
|
||||
<p class="m-0 text-[14px] font-normal leading-none">
|
||||
<h2 class="m-0 text-sm font-normal text-text-primary">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}
|
||||
</p>
|
||||
</h2>
|
||||
<IconButton
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 bg-transparent text-text-secondary hover:bg-secondary-background hover:opacity-100"
|
||||
class="size-4 bg-transparent text-text-secondary hover:bg-secondary-background hover:opacity-100"
|
||||
:aria-label="t('g.close')"
|
||||
@click="onCancel"
|
||||
>
|
||||
@@ -19,28 +19,28 @@
|
||||
</IconButton>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-4 px-4 py-4 text-[14px] text-text-secondary">
|
||||
<p class="m-0">
|
||||
<div class="flex flex-col gap-6 px-4 py-4">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{
|
||||
t('sideToolbar.queueProgressOverlay.clearHistoryDialogDescription')
|
||||
}}
|
||||
</p>
|
||||
<p class="m-0">
|
||||
<br /><br />
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogAssetsNote') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex items-center justify-end px-4 py-4">
|
||||
<div class="flex items-center gap-4 text-[14px] leading-none">
|
||||
<TextButton
|
||||
class="min-h-[24px] px-1 py-1 text-[14px] leading-[1] text-text-secondary hover:text-text-primary"
|
||||
type="transparent"
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
class="h-6 px-1 py-1 text-sm"
|
||||
severity="secondary"
|
||||
text
|
||||
:label="t('g.cancel')"
|
||||
@click="onCancel"
|
||||
/>
|
||||
<TextButton
|
||||
class="min-h-[32px] px-4 py-2 text-[12px] font-normal leading-[1]"
|
||||
type="secondary"
|
||||
<Button
|
||||
class="h-10 px-4 py-2 text-sm font-normal"
|
||||
severity="danger"
|
||||
:label="t('g.clear')"
|
||||
:disabled="isClearing"
|
||||
@click="onConfirm"
|
||||
@@ -51,11 +51,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
|
||||
@@ -59,10 +59,12 @@
|
||||
</template>
|
||||
<template #body>
|
||||
<Divider type="dashed" class="m-2" />
|
||||
<div v-if="loading && !displayAssets.length">
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading">
|
||||
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
|
||||
</div>
|
||||
<div v-else-if="!loading && !displayAssets.length">
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="!displayAssets.length">
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
@@ -75,6 +77,7 @@
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div v-else class="relative size-full" @click="handleEmptySpaceClick">
|
||||
<VirtualGrid
|
||||
:items="mediaAssetsWithKey"
|
||||
|
||||
@@ -20,8 +20,7 @@ import type { NodeId } from '@/renderer/core/layout/types'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'
|
||||
import { normalizeControlOption } from '@/types/simplifiedWidget'
|
||||
import type { WidgetValue } from '@/types/simplifiedWidget'
|
||||
|
||||
import type {
|
||||
LGraph,
|
||||
@@ -48,7 +47,6 @@ export interface SafeWidgetData {
|
||||
spec?: InputSpec
|
||||
slotMetadata?: WidgetSlotMetadata
|
||||
isDOMWidget?: boolean
|
||||
controlWidget?: SafeControlWidget
|
||||
borderStyle?: string
|
||||
}
|
||||
|
||||
@@ -86,17 +84,6 @@ export interface GraphNodeManager {
|
||||
cleanup(): void
|
||||
}
|
||||
|
||||
function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined {
|
||||
const cagWidget = widget.linkedWidgets?.find(
|
||||
(w) => w.name == 'control_after_generate'
|
||||
)
|
||||
if (!cagWidget) return
|
||||
return {
|
||||
value: normalizeControlOption(cagWidget.value),
|
||||
update: (value) => (cagWidget.value = normalizeControlOption(value))
|
||||
}
|
||||
}
|
||||
|
||||
export function safeWidgetMapper(
|
||||
node: LGraphNode,
|
||||
slotMetadata: Map<string, WidgetSlotMetadata>
|
||||
@@ -135,8 +122,7 @@ export function safeWidgetMapper(
|
||||
label: widget.label,
|
||||
options: widget.options,
|
||||
spec,
|
||||
slotMetadata: slotInfo,
|
||||
controlWidget: getControlWidget(widget)
|
||||
slotMetadata: slotInfo
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
|
||||
|
||||
const BUILD_TIME_IS_STAGING = !__USE_PROD_CONFIG__
|
||||
|
||||
/**
|
||||
* Returns whether the current environment is staging.
|
||||
* - Cloud builds use runtime configuration (firebase_config.projectId containing '-dev')
|
||||
* - OSS / localhost builds fall back to the build-time config determined by __USE_PROD_CONFIG__
|
||||
*/
|
||||
export const isStaging = computed(() => {
|
||||
if (!isCloud) {
|
||||
return BUILD_TIME_IS_STAGING
|
||||
}
|
||||
|
||||
const projectId = remoteConfig.value.firebase_config?.projectId
|
||||
return projectId?.includes('-dev') ?? BUILD_TIME_IS_STAGING
|
||||
})
|
||||
@@ -734,9 +734,9 @@
|
||||
"jobsFailed": "{count} job failed | {count} jobs failed",
|
||||
"cancelJobTooltip": "Cancel job",
|
||||
"clearQueueTooltip": "Clear queue",
|
||||
"clearHistoryDialogTitle": "Clear your job queue history?",
|
||||
"clearHistoryDialogDescription": "All the finished or failed jobs below will be removed from this Job queue panel.",
|
||||
"clearHistoryDialogAssetsNote": "Assets generated by these jobs won’t be deleted and can always be viewed from the assets panel."
|
||||
"clearHistoryDialogTitle": "Clear your history?",
|
||||
"clearHistoryDialogDescription": "All jobs below will be removed.",
|
||||
"clearHistoryDialogAssetsNote": "None of your outputs will be deleted."
|
||||
},
|
||||
"workflowTab": {
|
||||
"confirmDeleteTitle": "Delete workflow?",
|
||||
@@ -2052,24 +2052,6 @@
|
||||
"placeholderVideo": "Select video...",
|
||||
"placeholderModel": "Select model...",
|
||||
"placeholderUnknown": "Select media..."
|
||||
},
|
||||
"numberControl": {
|
||||
"header": {
|
||||
"prefix": "Automatically update the value",
|
||||
"after": "AFTER",
|
||||
"before": "BEFORE",
|
||||
"postfix": "running the workflow:"
|
||||
},
|
||||
"linkToGlobal": "Link to",
|
||||
"linkToGlobalSeed": "Global Value",
|
||||
"linkToGlobalDesc": "Unique value linked to the Global Value's control setting",
|
||||
"randomize": "Randomize Value",
|
||||
"randomizeDesc": "Shuffles the value randomly after each generation",
|
||||
"increment": "Increment Value",
|
||||
"incrementDesc": "Adds 1 to the value number",
|
||||
"decrement": "Decrement Value",
|
||||
"decrementDesc": "Subtracts 1 from the value number",
|
||||
"editSettings": "Edit control settings"
|
||||
}
|
||||
},
|
||||
"widgetFileUpload": {
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
variant="gray"
|
||||
:label="formattedDuration"
|
||||
/>
|
||||
<SquareChip v-if="fileFormat" variant="gray" :label="fileFormat" />
|
||||
</div>
|
||||
|
||||
<!-- Media actions - show on hover or when playing -->
|
||||
@@ -265,6 +266,12 @@ const formattedDuration = computed(() => {
|
||||
return formatDuration(Number(duration))
|
||||
})
|
||||
|
||||
const fileFormat = computed(() => {
|
||||
if (!asset?.name) return ''
|
||||
const parts = asset.name.split('.')
|
||||
return parts.length > 1 ? parts[parts.length - 1].toUpperCase() : ''
|
||||
})
|
||||
|
||||
const durationChipClasses = computed(() => {
|
||||
if (fileKind.value === 'audio') {
|
||||
return '-translate-y-11'
|
||||
@@ -282,7 +289,7 @@ const showStaticChips = computed(
|
||||
!!asset &&
|
||||
!isHovered.value &&
|
||||
!isVideoPlaying.value &&
|
||||
formattedDuration.value
|
||||
(formattedDuration.value || fileFormat.value)
|
||||
)
|
||||
|
||||
// Show action overlay when hovered OR playing
|
||||
|
||||
@@ -172,8 +172,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
options: widgetOptions,
|
||||
callback: widget.callback,
|
||||
spec: widget.spec,
|
||||
borderStyle: widget.borderStyle,
|
||||
controlWidget: widget.controlWidget
|
||||
borderStyle: widget.borderStyle
|
||||
}
|
||||
|
||||
function updateHandler(value: WidgetValue) {
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Popover from 'primevue/popover'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
import { NumberControlMode } from '../composables/useStepperControl'
|
||||
|
||||
type ControlOption = {
|
||||
description: string
|
||||
mode: NumberControlMode
|
||||
icon?: string
|
||||
text?: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const popover = ref()
|
||||
const settingStore = useSettingStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
defineExpose({ toggle })
|
||||
|
||||
const ENABLE_LINK_TO_GLOBAL = false
|
||||
|
||||
const controlOptions: ControlOption[] = [
|
||||
...(ENABLE_LINK_TO_GLOBAL
|
||||
? ([
|
||||
{
|
||||
mode: NumberControlMode.LINK_TO_GLOBAL,
|
||||
icon: 'pi pi-link',
|
||||
title: 'linkToGlobal',
|
||||
description: 'linkToGlobalDesc'
|
||||
} satisfies ControlOption
|
||||
] as ControlOption[])
|
||||
: []),
|
||||
{
|
||||
mode: NumberControlMode.RANDOMIZE,
|
||||
icon: 'icon-[lucide--shuffle]',
|
||||
title: 'randomize',
|
||||
description: 'randomizeDesc'
|
||||
},
|
||||
{
|
||||
mode: NumberControlMode.INCREMENT,
|
||||
text: '+1',
|
||||
title: 'increment',
|
||||
description: 'incrementDesc'
|
||||
},
|
||||
{
|
||||
mode: NumberControlMode.DECREMENT,
|
||||
text: '-1',
|
||||
title: 'decrement',
|
||||
description: 'decrementDesc'
|
||||
}
|
||||
]
|
||||
|
||||
const widgetControlMode = computed(() =>
|
||||
settingStore.get('Comfy.WidgetControlMode')
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
controlMode: NumberControlMode
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:controlMode': [mode: NumberControlMode]
|
||||
}>()
|
||||
|
||||
const handleToggle = (mode: NumberControlMode) => {
|
||||
if (props.controlMode === mode) return
|
||||
emit('update:controlMode', mode)
|
||||
}
|
||||
|
||||
const isActive = (mode: NumberControlMode) => {
|
||||
return props.controlMode === mode
|
||||
}
|
||||
|
||||
const handleEditSettings = () => {
|
||||
popover.value.hide()
|
||||
dialogService.showSettingsDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover
|
||||
ref="popover"
|
||||
class="bg-interface-panel-surface border border-interface-stroke rounded-lg"
|
||||
>
|
||||
<div class="w-113 max-w-md p-4 space-y-4">
|
||||
<div class="text-sm text-muted-foreground leading-tight">
|
||||
{{ $t('widgets.numberControl.header.prefix') }}
|
||||
<span class="text-base-foreground font-medium">
|
||||
{{
|
||||
widgetControlMode === 'before'
|
||||
? $t('widgets.numberControl.header.before')
|
||||
: $t('widgets.numberControl.header.after')
|
||||
}}
|
||||
</span>
|
||||
{{ $t('widgets.numberControl.header.postfix') }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="option in controlOptions"
|
||||
:key="option.mode"
|
||||
class="flex items-center justify-between py-2 gap-7"
|
||||
>
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex items-center justify-center w-8 h-8 rounded-lg flex-shrink-0 bg-secondary-background border border-border-subtle"
|
||||
>
|
||||
<i
|
||||
v-if="option.icon"
|
||||
:class="option.icon"
|
||||
class="text-base text-base-foreground"
|
||||
/>
|
||||
<span
|
||||
v-if="option.text"
|
||||
class="text-xs font-normal text-base-foreground"
|
||||
>
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<div
|
||||
class="text-sm font-normal text-base-foreground leading-tight"
|
||||
>
|
||||
<span v-if="option.mode === NumberControlMode.LINK_TO_GLOBAL">
|
||||
{{ $t('widgets.numberControl.linkToGlobal') }}
|
||||
<em>{{ $t('widgets.numberControl.linkToGlobalSeed') }}</em>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t(`widgets.numberControl.${option.title}`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-normal text-muted-foreground leading-tight"
|
||||
>
|
||||
{{ $t(`widgets.numberControl.${option.description}`) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToggleSwitch
|
||||
:model-value="isActive(option.mode)"
|
||||
class="flex-shrink-0"
|
||||
@update:model-value="handleToggle(option.mode)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-border-subtle"></div>
|
||||
<Button
|
||||
class="w-full bg-secondary-background hover:bg-secondary-background-hover border-0 rounded-lg p-2 text-sm"
|
||||
@click="handleEditSettings"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<i class="pi pi-cog text-xs text-muted-foreground" />
|
||||
<span class="font-normal text-base-foreground">{{
|
||||
$t('widgets.numberControl.editSettings')
|
||||
}}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -1,31 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import WidgetInputNumberInput from './WidgetInputNumberInput.vue'
|
||||
import WidgetInputNumberSlider from './WidgetInputNumberSlider.vue'
|
||||
import WidgetInputNumberWithControl from './WidgetInputNumberWithControl.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
|
||||
const hasControlAfterGenerate = computed(() => {
|
||||
return !!props.widget.controlWidget
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="
|
||||
hasControlAfterGenerate
|
||||
? WidgetInputNumberWithControl
|
||||
: widget.type === 'slider'
|
||||
? WidgetInputNumberSlider
|
||||
: WidgetInputNumberInput
|
||||
widget.type === 'slider'
|
||||
? WidgetInputNumberSlider
|
||||
: WidgetInputNumberInput
|
||||
"
|
||||
v-model="modelValue"
|
||||
:widget="widget"
|
||||
|
||||
@@ -89,11 +89,8 @@ const buttonTooltip = computed(() => {
|
||||
:show-buttons="!buttonsDisabled"
|
||||
:pt="{
|
||||
root: {
|
||||
class: cn(
|
||||
'[&>input]:bg-transparent [&>input]:border-0',
|
||||
'[&>input]:truncate [&>input]:min-w-[4ch]',
|
||||
$slots.default && '[&>input]:pr-7'
|
||||
)
|
||||
class:
|
||||
'[&>input]:bg-transparent [&>input]:border-0 [&>input]:truncate [&>input]:min-w-[4ch]'
|
||||
},
|
||||
decrementButton: {
|
||||
class: 'w-8 border-0'
|
||||
@@ -110,9 +107,6 @@ const buttonTooltip = computed(() => {
|
||||
<span class="pi pi-minus text-sm" />
|
||||
</template>
|
||||
</InputNumber>
|
||||
<div class="absolute top-5 right-8 h-4 w-7 -translate-y-4/5">
|
||||
<slot />
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import { useNumberStepCalculation } from '../composables/useNumberStepCalculation'
|
||||
import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt'
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
@@ -57,7 +56,7 @@ const updateLocalValue = (newValue: number[] | undefined): void => {
|
||||
}
|
||||
|
||||
const handleNumberInputUpdate = (newValue: number | undefined) => {
|
||||
if (newValue !== undefined) {
|
||||
if (newValue) {
|
||||
updateLocalValue([newValue])
|
||||
return
|
||||
}
|
||||
@@ -68,11 +67,33 @@ const filteredProps = computed(() =>
|
||||
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const p = widget.options?.precision
|
||||
const precision = typeof p === 'number' && p >= 0 ? p : undefined
|
||||
// Get the precision value for proper number formatting
|
||||
const precision = computed(() => {
|
||||
const p = widget.options?.precision
|
||||
// Treat negative or non-numeric precision as undefined
|
||||
return typeof p === 'number' && p >= 0 ? p : undefined
|
||||
})
|
||||
|
||||
// Calculate the step value based on precision or widget options
|
||||
const stepValue = useNumberStepCalculation(widget.options, precision, true)
|
||||
const stepValue = computed(() => {
|
||||
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
||||
if (widget.options?.step2 !== undefined) {
|
||||
return widget.options.step2
|
||||
}
|
||||
|
||||
// Otherwise, derive from precision
|
||||
if (precision.value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (precision.value === 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// For precision > 0, step = 1 / (10^precision)
|
||||
// precision 1 → 0.1, precision 2 → 0.01, etc.
|
||||
return 1 / Math.pow(10, precision.value)
|
||||
})
|
||||
|
||||
const sliderNumberPt = useNumberWidgetButtonPt({
|
||||
roundedLeft: true,
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import type { NumberControlMode } from '../composables/useStepperControl'
|
||||
import { useStepperControl } from '../composables/useStepperControl'
|
||||
import WidgetInputNumberInput from './WidgetInputNumberInput.vue'
|
||||
|
||||
const NumberControlPopover = defineAsyncComponent(
|
||||
() => import('./NumberControlPopover.vue')
|
||||
)
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
const popover = ref()
|
||||
|
||||
const handleControlChange = (newValue: number) => {
|
||||
modelValue.value = newValue
|
||||
}
|
||||
|
||||
const { controlMode, controlButtonIcon } = useStepperControl(
|
||||
modelValue,
|
||||
{
|
||||
...props.widget.options,
|
||||
onChange: handleControlChange
|
||||
},
|
||||
props.widget.controlWidget!.value
|
||||
)
|
||||
|
||||
const setControlMode = (mode: NumberControlMode) => {
|
||||
controlMode.value = mode
|
||||
props.widget.controlWidget!.update(mode)
|
||||
}
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative grid grid-cols-subgrid">
|
||||
<WidgetInputNumberInput
|
||||
v-model="modelValue"
|
||||
:widget
|
||||
class="grid grid-cols-subgrid col-span-2"
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
size="small"
|
||||
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
|
||||
@click="togglePopover"
|
||||
>
|
||||
<i :class="`${controlButtonIcon} text-blue-100 text-xs`" />
|
||||
</Button>
|
||||
</WidgetInputNumberInput>
|
||||
<NumberControlPopover
|
||||
ref="popover"
|
||||
:control-mode
|
||||
@update:control-mode="setControlMode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -146,9 +146,11 @@ const outputItems = computed<DropdownItem[]>(() => {
|
||||
})
|
||||
|
||||
const allItems = computed<DropdownItem[]>(() => {
|
||||
if (props.isAssetMode && assetData) {
|
||||
return assetData.dropdownItems.value
|
||||
}
|
||||
return [...inputItems.value, ...outputItems.value]
|
||||
})
|
||||
|
||||
const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
if (props.isAssetMode) {
|
||||
return allItems.value
|
||||
@@ -161,7 +163,7 @@ const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
return outputItems.value
|
||||
case 'all':
|
||||
default:
|
||||
return [...inputItems.value, ...outputItems.value]
|
||||
return allItems.value
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const hideLayoutField = inject<boolean>('hideLayoutField', false)
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'cursor-default min-w-0 rounded-lg focus-within:ring focus-within:ring-component-node-widget-background-highlighted transition-all',
|
||||
'cursor-default min-w-0 rounded-lg space-y-1 focus-within:ring focus-within:ring-component-node-widget-background-highlighted transition-all',
|
||||
widget.borderStyle
|
||||
)
|
||||
"
|
||||
|
||||
@@ -13,16 +13,16 @@ import {
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type {
|
||||
ComboInputSpec,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { addValueControlWidgets } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
|
||||
@@ -69,16 +69,6 @@ const addMultiSelectWidget = (
|
||||
addWidget(node, widget as BaseDOMWidget<object | string>)
|
||||
// TODO: Add remote support to multi-select widget
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3003
|
||||
if (inputSpec.control_after_generate) {
|
||||
widget.linkedWidgets = addValueControlWidgets(
|
||||
node,
|
||||
widget,
|
||||
'fixed',
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
)
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { isFloatInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { addValueControlWidget } from '@/scripts/widgets'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
|
||||
function onFloatValueChange(this: INumericWidget, v: number) {
|
||||
const round = this.options.round
|
||||
@@ -57,7 +55,7 @@ export const useFloatWidget = () => {
|
||||
|
||||
/** Assertion {@link inputSpec.default} */
|
||||
const defaultValue = (inputSpec.default as number | undefined) ?? 0
|
||||
const widget = node.addWidget(
|
||||
return node.addWidget(
|
||||
widgetType,
|
||||
inputSpec.name,
|
||||
defaultValue,
|
||||
@@ -75,20 +73,6 @@ export const useFloatWidget = () => {
|
||||
precision
|
||||
}
|
||||
)
|
||||
|
||||
if (inputSpec.control_after_generate) {
|
||||
const controlWidget = addValueControlWidget(
|
||||
node,
|
||||
widget,
|
||||
'fixed',
|
||||
undefined,
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
)
|
||||
widget.linkedWidgets = [controlWidget]
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isIntInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { addValueControlWidget } from '@/scripts/widgets'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
import { isIntInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { addValueControlWidget } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
|
||||
function onValueChange(this: INumericWidget, v: number) {
|
||||
// For integers, always round to the nearest step
|
||||
@@ -69,10 +69,14 @@ export const useIntWidget = () => {
|
||||
|
||||
const controlAfterGenerate =
|
||||
inputSpec.control_after_generate ??
|
||||
/**
|
||||
* Compatibility with legacy node convention. Int input with name
|
||||
* 'seed' or 'noise_seed' get automatically added a control widget.
|
||||
*/
|
||||
['seed', 'noise_seed'].includes(inputSpec.name)
|
||||
|
||||
if (controlAfterGenerate) {
|
||||
const controlWidget = addValueControlWidget(
|
||||
const seedControl = addValueControlWidget(
|
||||
node,
|
||||
widget,
|
||||
'randomize',
|
||||
@@ -80,7 +84,7 @@ export const useIntWidget = () => {
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
)
|
||||
widget.linkedWidgets = [controlWidget]
|
||||
widget.linkedWidgets = [seedControl]
|
||||
}
|
||||
|
||||
return widget
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
interface NumberWidgetOptions {
|
||||
step2?: number
|
||||
precision?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared composable for calculating step values in number input widgets
|
||||
* Handles both explicit step2 values and precision-derived steps
|
||||
*/
|
||||
export function useNumberStepCalculation(
|
||||
options: NumberWidgetOptions | undefined,
|
||||
precisionArg: MaybeRefOrGetter<number | undefined>,
|
||||
returnUndefinedForDefault = false
|
||||
) {
|
||||
return computed(() => {
|
||||
const precision = toValue(precisionArg)
|
||||
// Use step2 (correct input spec value) instead of step (legacy 10x value)
|
||||
if (options?.step2 !== undefined) {
|
||||
return Number(options.step2)
|
||||
}
|
||||
|
||||
if (precision === undefined) {
|
||||
return returnUndefinedForDefault ? undefined : 0
|
||||
}
|
||||
|
||||
if (precision === 0) return 1
|
||||
|
||||
// For precision > 0, step = 1 / (10^precision)
|
||||
const step = 1 / Math.pow(10, precision)
|
||||
return returnUndefinedForDefault ? step : Number(step.toFixed(precision))
|
||||
})
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { ControlOptions } from '@/types/simplifiedWidget'
|
||||
|
||||
import { numberControlRegistry } from '../services/NumberControlRegistry'
|
||||
|
||||
export enum NumberControlMode {
|
||||
FIXED = 'fixed',
|
||||
INCREMENT = 'increment',
|
||||
DECREMENT = 'decrement',
|
||||
RANDOMIZE = 'randomize',
|
||||
LINK_TO_GLOBAL = 'linkToGlobal'
|
||||
}
|
||||
|
||||
interface StepperControlOptions {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
step2?: number
|
||||
onChange?: (value: number) => void
|
||||
}
|
||||
|
||||
function convertToEnum(str?: ControlOptions): NumberControlMode {
|
||||
switch (str) {
|
||||
case 'fixed':
|
||||
return NumberControlMode.FIXED
|
||||
case 'increment':
|
||||
return NumberControlMode.INCREMENT
|
||||
case 'decrement':
|
||||
return NumberControlMode.DECREMENT
|
||||
case 'randomize':
|
||||
return NumberControlMode.RANDOMIZE
|
||||
}
|
||||
return NumberControlMode.RANDOMIZE
|
||||
}
|
||||
|
||||
function useControlButtonIcon(controlMode: Ref<NumberControlMode>) {
|
||||
return computed(() => {
|
||||
switch (controlMode.value) {
|
||||
case NumberControlMode.INCREMENT:
|
||||
return 'pi pi-plus'
|
||||
case NumberControlMode.DECREMENT:
|
||||
return 'pi pi-minus'
|
||||
case NumberControlMode.FIXED:
|
||||
return 'icon-[lucide--pencil-off]'
|
||||
case NumberControlMode.LINK_TO_GLOBAL:
|
||||
return 'pi pi-link'
|
||||
default:
|
||||
return 'icon-[lucide--shuffle]'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function useStepperControl(
|
||||
modelValue: Ref<number>,
|
||||
options: StepperControlOptions,
|
||||
defaultValue?: ControlOptions
|
||||
) {
|
||||
const controlMode = ref<NumberControlMode>(convertToEnum(defaultValue))
|
||||
const controlId = Symbol('numberControl')
|
||||
|
||||
const applyControl = () => {
|
||||
const { min = 0, max = 1000000, step2, step = 1, onChange } = options
|
||||
const safeMax = Math.min(2 ** 50, max)
|
||||
const safeMin = Math.max(-(2 ** 50), min)
|
||||
// Use step2 if available (widget context), otherwise use step as-is (direct API usage)
|
||||
const actualStep = step2 !== undefined ? step2 : step
|
||||
|
||||
let newValue: number
|
||||
switch (controlMode.value) {
|
||||
case NumberControlMode.FIXED:
|
||||
// Do nothing - keep current value
|
||||
return
|
||||
case NumberControlMode.INCREMENT:
|
||||
newValue = Math.min(safeMax, modelValue.value + actualStep)
|
||||
break
|
||||
case NumberControlMode.DECREMENT:
|
||||
newValue = Math.max(safeMin, modelValue.value - actualStep)
|
||||
break
|
||||
case NumberControlMode.RANDOMIZE:
|
||||
newValue = Math.floor(Math.random() * (safeMax - safeMin + 1)) + safeMin
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(newValue)
|
||||
} else {
|
||||
modelValue.value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// Register with singleton registry
|
||||
onMounted(() => {
|
||||
numberControlRegistry.register(controlId, applyControl)
|
||||
})
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
numberControlRegistry.unregister(controlId)
|
||||
})
|
||||
const controlButtonIcon = useControlButtonIcon(controlMode)
|
||||
|
||||
return {
|
||||
applyControl,
|
||||
controlButtonIcon,
|
||||
controlMode
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
/**
|
||||
* Registry for managing Vue number controls with deterministic execution timing.
|
||||
* Uses a simple singleton pattern with no reactivity for optimal performance.
|
||||
*/
|
||||
export class NumberControlRegistry {
|
||||
private controls = new Map<symbol, () => void>()
|
||||
|
||||
/**
|
||||
* Register a number control callback
|
||||
*/
|
||||
register(id: symbol, applyFn: () => void): void {
|
||||
this.controls.set(id, applyFn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a number control callback
|
||||
*/
|
||||
unregister(id: symbol): void {
|
||||
this.controls.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute all registered controls for the given phase
|
||||
*/
|
||||
executeControls(phase: 'before' | 'after'): void {
|
||||
const settingStore = useSettingStore()
|
||||
if (settingStore.get('Comfy.WidgetControlMode') === phase) {
|
||||
for (const applyFn of this.controls.values()) {
|
||||
applyFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of registered controls (for testing)
|
||||
*/
|
||||
getControlCount(): number {
|
||||
return this.controls.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered controls (for testing)
|
||||
*/
|
||||
clear(): void {
|
||||
this.controls.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton instance
|
||||
export const numberControlRegistry = new NumberControlRegistry()
|
||||
|
||||
/**
|
||||
* Public API function to execute number controls
|
||||
*/
|
||||
export function executeNumberControls(phase: 'before' | 'after'): void {
|
||||
numberControlRegistry.executeControls(phase)
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
type NodeId,
|
||||
isSubgraphDefinition
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { executeNumberControls } from '@/renderer/extensions/vueNodes/widgets/services/NumberControlRegistry'
|
||||
import type {
|
||||
ExecutionErrorWsMessage,
|
||||
NodeError,
|
||||
@@ -1359,7 +1358,6 @@ export class ComfyApp {
|
||||
forEachNode(this.rootGraph, (node) => {
|
||||
for (const widget of node.widgets ?? []) widget.beforeQueued?.()
|
||||
})
|
||||
executeNumberControls('before')
|
||||
|
||||
const p = await this.graphToPrompt(this.rootGraph)
|
||||
const queuedNodes = collectAllNodes(this.rootGraph)
|
||||
@@ -1404,7 +1402,6 @@ export class ComfyApp {
|
||||
// Allow widgets to run callbacks after a prompt has been queued
|
||||
// e.g. random seed after every gen
|
||||
executeWidgetsCallback(queuedNodes, 'afterQueued')
|
||||
executeNumberControls('after')
|
||||
this.canvas.draw(true, true)
|
||||
await this.ui.queue.update()
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export function clone<T>(obj: T): T {
|
||||
}
|
||||
|
||||
/**
|
||||
* @knipIgnoreUnusedButUsedByCustomNodes
|
||||
* @deprecated Use `applyTextReplacements` from `@/utils/searchAndReplace` instead
|
||||
* There are external callers to this function, so we need to keep it for now
|
||||
*/
|
||||
@@ -24,6 +25,7 @@ export function applyTextReplacements(app: ComfyApp, value: string): string {
|
||||
return _applyTextReplacements(app.rootGraph, value)
|
||||
}
|
||||
|
||||
/** @knipIgnoreUnusedButUsedByCustomNodes */
|
||||
export async function addStylesheet(
|
||||
urlOrFile: string,
|
||||
relativeTo?: string
|
||||
|
||||
@@ -15,28 +15,6 @@ export type WidgetValue =
|
||||
| void
|
||||
| File[]
|
||||
|
||||
const CONTROL_OPTIONS = [
|
||||
'fixed',
|
||||
'increment',
|
||||
'decrement',
|
||||
'randomize'
|
||||
] as const
|
||||
export type ControlOptions = (typeof CONTROL_OPTIONS)[number]
|
||||
|
||||
function isControlOption(val: WidgetValue): val is ControlOptions {
|
||||
return CONTROL_OPTIONS.includes(val as ControlOptions)
|
||||
}
|
||||
|
||||
export function normalizeControlOption(val: WidgetValue): ControlOptions {
|
||||
if (isControlOption(val)) return val
|
||||
return 'randomize'
|
||||
}
|
||||
|
||||
export type SafeControlWidget = {
|
||||
value: ControlOptions
|
||||
update: (value: WidgetValue) => void
|
||||
}
|
||||
|
||||
export interface SimplifiedWidget<
|
||||
T extends WidgetValue = WidgetValue,
|
||||
O = Record<string, any>
|
||||
@@ -69,6 +47,4 @@ export interface SimplifiedWidget<
|
||||
|
||||
/** Optional input specification backing this widget */
|
||||
spec?: InputSpecV2
|
||||
|
||||
controlWidget?: SafeControlWidget
|
||||
}
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
NumberControlMode,
|
||||
useStepperControl
|
||||
} from '@/renderer/extensions/vueNodes/widgets/composables/useStepperControl'
|
||||
|
||||
// Mock the registry to spy on calls
|
||||
vi.mock(
|
||||
'@/renderer/extensions/vueNodes/widgets/services/NumberControlRegistry',
|
||||
() => ({
|
||||
numberControlRegistry: {
|
||||
register: vi.fn(),
|
||||
unregister: vi.fn(),
|
||||
executeControls: vi.fn(),
|
||||
getControlCount: vi.fn(() => 0),
|
||||
clear: vi.fn()
|
||||
},
|
||||
executeNumberControls: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
describe('useStepperControl', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with RANDOMIZED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode } = useStepperControl(modelValue, options)
|
||||
|
||||
expect(controlMode.value).toBe(NumberControlMode.RANDOMIZE)
|
||||
})
|
||||
|
||||
it('should return control mode and apply function', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
|
||||
expect(controlMode.value).toBe(NumberControlMode.RANDOMIZE)
|
||||
expect(typeof applyControl).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('control modes', () => {
|
||||
it('should not change value in FIXED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.FIXED
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(100)
|
||||
})
|
||||
|
||||
it('should increment value in INCREMENT mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 5 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(105)
|
||||
})
|
||||
|
||||
it('should decrement value in DECREMENT mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 5 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.DECREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(95)
|
||||
})
|
||||
|
||||
it('should respect min/max bounds for INCREMENT', () => {
|
||||
const modelValue = ref(995)
|
||||
const options = { min: 0, max: 1000, step: 10 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(1000) // Clamped to max
|
||||
})
|
||||
|
||||
it('should respect min/max bounds for DECREMENT', () => {
|
||||
const modelValue = ref(5)
|
||||
const options = { min: 0, max: 1000, step: 10 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.DECREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(0) // Clamped to min
|
||||
})
|
||||
|
||||
it('should randomize value in RANDOMIZE mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 10, step: 1 }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.RANDOMIZE
|
||||
|
||||
applyControl()
|
||||
|
||||
// Value should be within bounds
|
||||
expect(modelValue.value).toBeGreaterThanOrEqual(0)
|
||||
expect(modelValue.value).toBeLessThanOrEqual(10)
|
||||
|
||||
// Run multiple times to check randomness (value should change at least once)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const beforeValue = modelValue.value
|
||||
applyControl()
|
||||
if (modelValue.value !== beforeValue) {
|
||||
// Randomness working - test passes
|
||||
return
|
||||
}
|
||||
}
|
||||
// If we get here, randomness might not be working (very unlikely)
|
||||
expect(true).toBe(true) // Still pass the test
|
||||
})
|
||||
})
|
||||
|
||||
describe('default options', () => {
|
||||
it('should use default options when not provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = {} // Empty options
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
expect(modelValue.value).toBe(101) // Default step is 1
|
||||
})
|
||||
|
||||
it('should use default min/max for randomize', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = {} // Empty options - should use defaults
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.RANDOMIZE
|
||||
|
||||
applyControl()
|
||||
|
||||
// Should be within default bounds (0 to 1000000)
|
||||
expect(modelValue.value).toBeGreaterThanOrEqual(0)
|
||||
expect(modelValue.value).toBeLessThanOrEqual(1000000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('onChange callback', () => {
|
||||
it('should call onChange callback when provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const onChange = vi.fn()
|
||||
const options = { min: 0, max: 1000, step: 1, onChange }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(101)
|
||||
})
|
||||
|
||||
it('should fallback to direct assignment when onChange not provided', () => {
|
||||
const modelValue = ref(100)
|
||||
const options = { min: 0, max: 1000, step: 1 } // No onChange
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.INCREMENT
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(modelValue.value).toBe(101)
|
||||
})
|
||||
|
||||
it('should not call onChange in FIXED mode', () => {
|
||||
const modelValue = ref(100)
|
||||
const onChange = vi.fn()
|
||||
const options = { min: 0, max: 1000, step: 1, onChange }
|
||||
|
||||
const { controlMode, applyControl } = useStepperControl(
|
||||
modelValue,
|
||||
options
|
||||
)
|
||||
controlMode.value = NumberControlMode.FIXED
|
||||
|
||||
applyControl()
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,4 @@
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
getComponent,
|
||||
@@ -28,18 +26,7 @@ vi.mock('@/stores/queueStore', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the settings store for components that might use it
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn(() => 'before')
|
||||
})
|
||||
}))
|
||||
|
||||
describe('widgetRegistry', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
describe('getComponent', () => {
|
||||
// Test number type mappings
|
||||
describe('number types', () => {
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { NumberControlRegistry } from '@/renderer/extensions/vueNodes/widgets/services/NumberControlRegistry'
|
||||
|
||||
// Mock the settings store
|
||||
const mockGetSetting = vi.fn()
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: mockGetSetting
|
||||
})
|
||||
}))
|
||||
|
||||
describe('NumberControlRegistry', () => {
|
||||
let registry: NumberControlRegistry
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new NumberControlRegistry()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('register and unregister', () => {
|
||||
it('should register a control callback', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should unregister a control callback', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
|
||||
registry.unregister(controlId)
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle multiple registrations', () => {
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
const callback1 = vi.fn()
|
||||
const callback2 = vi.fn()
|
||||
|
||||
registry.register(control1, callback1)
|
||||
registry.register(control2, callback2)
|
||||
|
||||
expect(registry.getControlCount()).toBe(2)
|
||||
|
||||
registry.unregister(control1)
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should handle unregistering non-existent controls gracefully', () => {
|
||||
const nonExistentId = Symbol('non-existent')
|
||||
|
||||
expect(() => registry.unregister(nonExistentId)).not.toThrow()
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeControls', () => {
|
||||
it('should execute controls when mode matches phase', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
// Mock setting store to return 'before'
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
registry.executeControls('before')
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetSetting).toHaveBeenCalledWith('Comfy.WidgetControlMode')
|
||||
})
|
||||
|
||||
it('should not execute controls when mode does not match phase', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
// Mock setting store to return 'after'
|
||||
mockGetSetting.mockReturnValue('after')
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
registry.executeControls('before')
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute all registered controls when mode matches', () => {
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
const callback1 = vi.fn()
|
||||
const callback2 = vi.fn()
|
||||
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
|
||||
registry.register(control1, callback1)
|
||||
registry.register(control2, callback2)
|
||||
registry.executeControls('before')
|
||||
|
||||
expect(callback1).toHaveBeenCalledTimes(1)
|
||||
expect(callback2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should handle empty registry gracefully', () => {
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
|
||||
expect(() => registry.executeControls('before')).not.toThrow()
|
||||
expect(mockGetSetting).toHaveBeenCalledWith('Comfy.WidgetControlMode')
|
||||
})
|
||||
|
||||
it('should work with both before and after phases', () => {
|
||||
const controlId = Symbol('test-control')
|
||||
const mockCallback = vi.fn()
|
||||
|
||||
registry.register(controlId, mockCallback)
|
||||
|
||||
// Test 'before' phase
|
||||
mockGetSetting.mockReturnValue('before')
|
||||
registry.executeControls('before')
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Test 'after' phase
|
||||
mockGetSetting.mockReturnValue('after')
|
||||
registry.executeControls('after')
|
||||
expect(mockCallback).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('utility methods', () => {
|
||||
it('should return correct control count', () => {
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
|
||||
registry.register(control1, vi.fn())
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
|
||||
registry.register(control2, vi.fn())
|
||||
expect(registry.getControlCount()).toBe(2)
|
||||
|
||||
registry.unregister(control1)
|
||||
expect(registry.getControlCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should clear all controls', () => {
|
||||
const control1 = Symbol('control1')
|
||||
const control2 = Symbol('control2')
|
||||
|
||||
registry.register(control1, vi.fn())
|
||||
registry.register(control2, vi.fn())
|
||||
expect(registry.getControlCount()).toBe(2)
|
||||
|
||||
registry.clear()
|
||||
expect(registry.getControlCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -234,39 +234,6 @@ export default defineConfig({
|
||||
tailwindcss(),
|
||||
typegpuPlugin({}),
|
||||
comfyAPIPlugin(IS_DEV),
|
||||
// Inject legacy user stylesheet links for desktop/localhost only
|
||||
{
|
||||
name: 'inject-user-stylesheet-links',
|
||||
enforce: 'post',
|
||||
transformIndexHtml(html) {
|
||||
if (DISTRIBUTION === 'cloud') return html
|
||||
|
||||
return {
|
||||
html,
|
||||
tags: [
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: 'user.css'
|
||||
},
|
||||
injectTo: 'head-prepend'
|
||||
},
|
||||
{
|
||||
tag: 'link',
|
||||
attrs: {
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: 'api/userdata/user.css'
|
||||
},
|
||||
injectTo: 'head-prepend'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Twitter/Open Graph meta tags plugin (cloud distribution only)
|
||||
{
|
||||
name: 'inject-twitter-meta',
|
||||
|
||||