mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-07 06:00:03 +00:00
Merge remote-tracking branch 'origin/queue-overlay-deletions' into queue-overlay-additions
This commit is contained in:
@@ -15,14 +15,7 @@
|
||||
:style="style"
|
||||
class="flex flex-col items-stretch"
|
||||
>
|
||||
<Panel
|
||||
ref="panelRef"
|
||||
:class="panelRootClass"
|
||||
:pt="{
|
||||
header: { class: 'hidden' },
|
||||
content: { class: docked ? 'p-0' : 'p-1' }
|
||||
}"
|
||||
>
|
||||
<div ref="panelRef" :class="cn(panelRootClass, docked ? 'p-0' : 'p-1')">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center select-none">
|
||||
<span
|
||||
@@ -35,7 +28,9 @@
|
||||
"
|
||||
/>
|
||||
|
||||
<ComfyRunButton />
|
||||
<Suspense @resolve="comfyRunButtonResolved">
|
||||
<ComfyRunButton />
|
||||
</Suspense>
|
||||
<IconButton
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
type="transparent"
|
||||
@@ -74,7 +69,7 @@
|
||||
</IconTextButton>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<div v-if="isFloating" class="flex justify-end pt-1 pr-1">
|
||||
<QueueInlineProgressSummary
|
||||
@@ -103,9 +98,7 @@ import {
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Panel from 'primevue/panel'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
@@ -143,7 +136,7 @@ const visible = computed(() => position.value !== 'Disabled')
|
||||
|
||||
const tabContainer = document.querySelector('.workflow-tabs-container')
|
||||
const actionbarWrapperRef = ref<HTMLElement | null>(null)
|
||||
const panelRef = ref<HTMLElement | ComponentPublicInstance | null>(null)
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const docked = computed({
|
||||
get: () => props.docked ?? false,
|
||||
@@ -244,7 +237,14 @@ const setInitialPosition = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
onMounted(setInitialPosition)
|
||||
|
||||
//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component.
|
||||
//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition()
|
||||
async function comfyRunButtonResolved() {
|
||||
await nextTick()
|
||||
setInitialPosition()
|
||||
}
|
||||
|
||||
watch(visible, async (newVisible) => {
|
||||
if (newVisible) {
|
||||
await nextTick(setInitialPosition)
|
||||
|
||||
@@ -58,7 +58,7 @@ const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hasMissingNodes = computed(() =>
|
||||
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
|
||||
graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName)
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -83,7 +83,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const hasMissingNodes = computed(() =>
|
||||
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
|
||||
graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName)
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import SearchBox from './SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers'
|
||||
interface GenericMeta<C> extends Omit<Meta<C>, 'component'> {
|
||||
component: Omit<ComponentExposed<C>, 'focus'>
|
||||
}
|
||||
|
||||
const meta: Meta<typeof SearchBox> = {
|
||||
const meta: GenericMeta<typeof SearchBox> = {
|
||||
title: 'Components/Input/SearchBox',
|
||||
component: SearchBox,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
modelValue: {
|
||||
control: 'text'
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text'
|
||||
},
|
||||
@@ -19,9 +26,12 @@ const meta: Meta<typeof SearchBox> = {
|
||||
control: 'select',
|
||||
options: ['md', 'lg'],
|
||||
description: 'Size variant of the search box'
|
||||
}
|
||||
},
|
||||
'onUpdate:modelValue': { action: 'update:modelValue' },
|
||||
onSearch: { action: 'search' }
|
||||
},
|
||||
args: {
|
||||
modelValue: '',
|
||||
placeholder: 'Search...',
|
||||
showBorder: false,
|
||||
size: 'md'
|
||||
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from './SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -50,15 +50,15 @@ describe('SearchBox', () => {
|
||||
await input.setValue('test')
|
||||
|
||||
// Model should not update immediately
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Advance timers by 299ms (just before debounce delay)
|
||||
vi.advanceTimersByTime(299)
|
||||
await vi.advanceTimersByTimeAsync(299)
|
||||
await nextTick()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Advance timers by 1ms more (reaching 300ms)
|
||||
vi.advanceTimersByTime(1)
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await nextTick()
|
||||
|
||||
// Model should now be updated
|
||||
@@ -82,19 +82,19 @@ describe('SearchBox', () => {
|
||||
|
||||
// Type third character (should reset timer again)
|
||||
await input.setValue('tes')
|
||||
vi.advanceTimersByTime(200)
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await nextTick()
|
||||
|
||||
// Should not have emitted yet (only 200ms passed since last keystroke)
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Advance final 100ms to reach 300ms
|
||||
vi.advanceTimersByTime(100)
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
await nextTick()
|
||||
|
||||
// Should now emit with final value
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['tes'])
|
||||
expect(wrapper.emitted('search')).toBeTruthy()
|
||||
expect(wrapper.emitted('search')?.[0]).toEqual(['tes', []])
|
||||
})
|
||||
|
||||
it('should only emit final value after rapid typing', async () => {
|
||||
@@ -105,19 +105,20 @@ describe('SearchBox', () => {
|
||||
const searchTerms = ['s', 'se', 'sea', 'sear', 'searc', 'search']
|
||||
for (const term of searchTerms) {
|
||||
await input.setValue(term)
|
||||
vi.advanceTimersByTime(50) // Less than debounce delay
|
||||
await vi.advanceTimersByTimeAsync(50) // Less than debounce delay
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
// Should not have emitted yet
|
||||
expect(wrapper.emitted('update:modelValue')).toBeFalsy()
|
||||
expect(wrapper.emitted('search')).toBeFalsy()
|
||||
|
||||
// Complete the debounce delay
|
||||
vi.advanceTimersByTime(300)
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
await nextTick()
|
||||
|
||||
// Should emit only once with final value
|
||||
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['search'])
|
||||
expect(wrapper.emitted('search')).toHaveLength(1)
|
||||
expect(wrapper.emitted('search')?.[0]).toEqual(['search', []])
|
||||
})
|
||||
|
||||
describe('bidirectional model sync', () => {
|
||||
@@ -1,84 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
<IconField>
|
||||
<Button
|
||||
v-if="filterIcon"
|
||||
class="p-inputicon filter-button"
|
||||
:icon="filterIcon"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="$emit('showFilter', $event)"
|
||||
/>
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
class="search-box-input w-full"
|
||||
:model-value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:autofocus="autofocus"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="clearSearch"
|
||||
/>
|
||||
</IconField>
|
||||
<div
|
||||
v-if="filters?.length"
|
||||
class="search-filters flex flex-wrap gap-2 pt-2"
|
||||
>
|
||||
<SearchFilterChip
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:text="filter.text"
|
||||
:badge="filter.badge"
|
||||
:badge-class="filter.badgeClass"
|
||||
@remove="$emit('removeFilter', filter)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full items-center gap-2 bg-comfy-input cursor-text text-comfy-input-foreground',
|
||||
customClass,
|
||||
wrapperStyle
|
||||
)
|
||||
"
|
||||
>
|
||||
<InputText
|
||||
ref="inputRef"
|
||||
v-model="modelValue"
|
||||
:placeholder
|
||||
:autofocus
|
||||
unstyled
|
||||
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm"
|
||||
:aria-label="placeholder"
|
||||
/>
|
||||
<IconButton
|
||||
v-if="filterIcon"
|
||||
class="p-inputicon filter-button absolute right-0 inset-y-0 h-full m-0 p-0"
|
||||
:icon="filterIcon"
|
||||
severity="contrast"
|
||||
@click="$emit('showFilter', $event)"
|
||||
/>
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="modelValue = ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="filters?.length" class="search-filters flex flex-wrap gap-2 pt-2">
|
||||
<SearchFilterChip
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
:text="filter.text"
|
||||
:badge="filter.badge"
|
||||
:badge-class="filter.badgeClass"
|
||||
@remove="$emit('removeFilter', filter)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import IconButton from '../button/IconButton.vue'
|
||||
import type { SearchFilter } from './SearchFilterChip.vue'
|
||||
import SearchFilterChip from './SearchFilterChip.vue'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
placeholder = 'Search...',
|
||||
icon = 'pi pi-search',
|
||||
debounceTime = 300,
|
||||
filterIcon,
|
||||
filters = [],
|
||||
autofocus = false
|
||||
autofocus = false,
|
||||
showBorder = false,
|
||||
size = 'md',
|
||||
class: customClass
|
||||
} = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
filterIcon?: string
|
||||
filters?: TFilter[]
|
||||
autofocus?: boolean
|
||||
showBorder?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'search', value: string, filters: TFilter[]): void
|
||||
(e: 'showFilter', event: Event): void
|
||||
(e: 'removeFilter', filter: TFilter): void
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
|
||||
const inputRef = ref()
|
||||
|
||||
defineExpose({
|
||||
@@ -87,20 +96,27 @@ defineExpose({
|
||||
}
|
||||
})
|
||||
|
||||
const emitSearch = debounce((value: string) => {
|
||||
emit('search', value, filters)
|
||||
}, debounceTime)
|
||||
watchDebounced(
|
||||
modelValue,
|
||||
(value: string) => {
|
||||
emit('search', value, filters)
|
||||
},
|
||||
{ debounce: debounceTime }
|
||||
)
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
emitSearch(target.value)
|
||||
}
|
||||
const wrapperStyle = computed(() => {
|
||||
if (showBorder) {
|
||||
return cn('rounded p-2 border border-solid border-border-default')
|
||||
}
|
||||
|
||||
const clearSearch = () => {
|
||||
emit('update:modelValue', '')
|
||||
emitSearch('')
|
||||
}
|
||||
// Size-specific classes matching button sizes for consistency
|
||||
const sizeClasses = {
|
||||
md: 'h-8 px-2 py-1.5', // Matches button sm size
|
||||
lg: 'h-10 px-4 py-2' // Matches button md size
|
||||
}[size]
|
||||
|
||||
return cn('rounded-lg', sizeClasses)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -64,7 +64,8 @@ const formattedCreditsOnly = computed(() => {
|
||||
const cents = authStore.balance?.amount_micros ?? 0
|
||||
const amount = formatCreditsFromCents({
|
||||
cents,
|
||||
locale: locale.value
|
||||
locale: locale.value,
|
||||
numberOptions: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
|
||||
})
|
||||
return amount
|
||||
})
|
||||
|
||||
@@ -388,8 +388,8 @@ import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue'
|
||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||
|
||||
@@ -128,7 +128,7 @@ onMounted(async () => {
|
||||
reportContent.value = generateErrorReport({
|
||||
systemStats: systemStatsStore.systemStats!,
|
||||
serverLogs: logs,
|
||||
workflow: app.graph.serialize(),
|
||||
workflow: app.rootGraph.serialize(),
|
||||
exceptionType: error.exceptionType,
|
||||
exceptionMessage: error.exceptionMessage,
|
||||
traceback: error.traceback,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-semibold text-white m-0">
|
||||
<h1 class="text-2xl font-semibold text-base-foreground m-0">
|
||||
{{
|
||||
isInsufficientCredits
|
||||
? $t('credits.topUp.addMoreCreditsToRun')
|
||||
@@ -62,7 +62,7 @@
|
||||
severity="primary"
|
||||
:label="$t('credits.topUp.buy')"
|
||||
:class="['w-full', { 'opacity-30': !selectedCredits || loading }]"
|
||||
:pt="{ label: { class: 'text-white' } }"
|
||||
:pt="{ label: { class: 'text-primary-foreground' } }"
|
||||
@click="handleBuy"
|
||||
/>
|
||||
</div>
|
||||
@@ -122,11 +122,7 @@ import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
creditsToUsd,
|
||||
formatCredits,
|
||||
formatUsd
|
||||
} from '@/base/credits/comfyCredits'
|
||||
import { creditsToUsd } from '@/base/credits/comfyCredits'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
@@ -156,7 +152,7 @@ const { formattedRenewalDate } = useSubscription()
|
||||
// Use feature flag to determine design - defaults to true (new design)
|
||||
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const telemetry = useTelemetry()
|
||||
const toast = useToast()
|
||||
@@ -191,19 +187,6 @@ const handleBuy = async () => {
|
||||
const usdAmount = creditsToUsd(selectedCredits.value)
|
||||
telemetry?.trackApiCreditTopupButtonPurchaseClicked(usdAmount)
|
||||
await authActions.purchaseCredits(usdAmount)
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('credits.topUp.purchaseSuccess'),
|
||||
detail: t('credits.topUp.purchaseSuccessDetail', {
|
||||
credits: formatCredits({
|
||||
value: selectedCredits.value,
|
||||
locale: locale.value
|
||||
}),
|
||||
amount: `$${formatUsd({ value: usdAmount, locale: locale.value })}`
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Purchase failed:', error)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
]"
|
||||
@click="$emit('select')"
|
||||
>
|
||||
<span class="text-base font-bold text-white">
|
||||
<span class="text-base font-bold text-base-foreground">
|
||||
{{ formattedCredits }}
|
||||
</span>
|
||||
<span class="text-sm font-normal text-white">
|
||||
<span class="text-sm font-normal text-muted-foreground">
|
||||
{{ description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -60,12 +60,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="h-8 w-8" />
|
||||
<ProgressSpinner v-if="loading" class="mx-auto h-8 w-8" />
|
||||
<Button
|
||||
v-else
|
||||
type="submit"
|
||||
:label="t('auth.login.loginButton')"
|
||||
class="mt-4 h-10 font-medium"
|
||||
:disabled="!$form.valid"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -74,6 +75,7 @@
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
@@ -100,11 +102,11 @@ const emit = defineEmits<{
|
||||
|
||||
const emailInputId = 'comfy-org-sign-in-email'
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
const onSubmit = useThrottleFn((event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
}, 1_500)
|
||||
|
||||
const handleForgotPassword = async (
|
||||
email: string,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
class="flex flex-col gap-6"
|
||||
:resolver="zodResolver(signUpSchema)"
|
||||
@submit="onSubmit"
|
||||
@@ -28,10 +29,13 @@
|
||||
<PasswordFields />
|
||||
|
||||
<!-- Submit Button -->
|
||||
<ProgressSpinner v-if="loading" class="mx-auto h-8 w-8" />
|
||||
<Button
|
||||
v-else
|
||||
type="submit"
|
||||
:label="t('auth.signup.signUpButton')"
|
||||
class="mt-4 h-10 font-medium"
|
||||
:disabled="!$form.valid"
|
||||
/>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -40,24 +44,30 @@
|
||||
import type { FormSubmitEvent } from '@primevue/forms'
|
||||
import { Form, FormField } from '@primevue/forms'
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { signUpSchema } from '@/schemas/signInSchema'
|
||||
import type { SignUpData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
import PasswordFields from './PasswordFields.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const loading = computed(() => authStore.loading)
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [values: SignUpData]
|
||||
}>()
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
const onSubmit = useThrottleFn((event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignUpData)
|
||||
}
|
||||
}
|
||||
}, 1_500)
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
synced with the stateStorage (localStorage). -->
|
||||
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
|
||||
<template v-if="showUI" #workflow-tabs>
|
||||
<TryVueNodeBanner />
|
||||
<div
|
||||
v-if="workflowTabsPosition === 'Topbar'"
|
||||
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
|
||||
@@ -160,8 +159,8 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isNativeWindow } from '@/utils/envUtil'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import TryVueNodeBanner from '../topbar/TryVueNodeBanner.vue'
|
||||
import SelectionRectangle from './SelectionRectangle.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -271,20 +270,18 @@ watch(
|
||||
() => {
|
||||
if (!canvasStore.canvas) return
|
||||
|
||||
for (const n of comfyApp.graph.nodes) {
|
||||
if (!n.widgets) continue
|
||||
forEachNode(comfyApp.rootGraph, (n) => {
|
||||
if (!n.widgets) return
|
||||
for (const w of n.widgets) {
|
||||
if (w[IS_CONTROL_WIDGET]) {
|
||||
updateControlWidgetLabel(w)
|
||||
if (w.linkedWidgets) {
|
||||
for (const l of w.linkedWidgets) {
|
||||
updateControlWidgetLabel(l)
|
||||
}
|
||||
}
|
||||
if (!w[IS_CONTROL_WIDGET]) continue
|
||||
updateControlWidgetLabel(w)
|
||||
if (!w.linkedWidgets) continue
|
||||
for (const l of w.linkedWidgets) {
|
||||
updateControlWidgetLabel(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
comfyApp.graph.setDirtyCanvas(true)
|
||||
})
|
||||
canvasStore.canvas.setDirty(true)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -334,7 +331,7 @@ watch(
|
||||
}
|
||||
|
||||
// Force canvas redraw to ensure progress updates are visible
|
||||
canvas.graph.setDirtyCanvas(true, false)
|
||||
canvas.setDirty(true, false)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -346,7 +343,7 @@ watch(
|
||||
(lastNodeErrors) => {
|
||||
if (!comfyApp.graph) return
|
||||
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
forEachNode(comfyApp.rootGraph, (node) => {
|
||||
// Clear existing errors
|
||||
for (const slot of node.inputs) {
|
||||
delete slot.hasErrors
|
||||
@@ -356,7 +353,7 @@ watch(
|
||||
}
|
||||
|
||||
const nodeErrors = lastNodeErrors?.[node.id]
|
||||
if (!nodeErrors) continue
|
||||
if (!nodeErrors) return
|
||||
|
||||
const validErrors = nodeErrors.errors.filter(
|
||||
(error) => error.extra_info?.input_name !== undefined
|
||||
@@ -369,9 +366,9 @@ watch(
|
||||
node.inputs[inputIndex].hasErrors = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
comfyApp.canvas.draw(true, true)
|
||||
comfyApp.canvas.setDirty(true, true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const onEdit = (newValue: string) => {
|
||||
target.subgraph.name = trimmedTitle
|
||||
}
|
||||
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
showInput.value = false
|
||||
titleEditorStore.titleEditorTarget = null
|
||||
|
||||
@@ -178,7 +178,7 @@ import MultiSelect from 'primevue/multiselect'
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { usePopoverSizing } from '@/composables/usePopoverSizing'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<template>
|
||||
<div :class="wrapperStyle" @click="focusInput">
|
||||
<i class="icon-[lucide--search] text-muted-foreground" />
|
||||
<InputText
|
||||
ref="input"
|
||||
v-model="internalSearchQuery"
|
||||
:aria-label="
|
||||
placeholder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
|
||||
"
|
||||
:placeholder="
|
||||
placeholder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
|
||||
"
|
||||
type="text"
|
||||
unstyled
|
||||
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const SEARCH_DEBOUNCE_DELAY_MS = 300
|
||||
|
||||
const {
|
||||
autofocus = false,
|
||||
placeholder,
|
||||
showBorder = false,
|
||||
size = 'md'
|
||||
} = defineProps<{
|
||||
autofocus?: boolean
|
||||
placeholder?: string
|
||||
showBorder?: boolean
|
||||
size?: 'md' | 'lg'
|
||||
}>()
|
||||
|
||||
// defineModel without arguments uses 'modelValue' as the prop name
|
||||
const searchQuery = defineModel<string>()
|
||||
|
||||
// Internal search query state for immediate UI updates
|
||||
const internalSearchQuery = ref<string>(searchQuery.value ?? '')
|
||||
|
||||
// Create debounced function to update the parent model
|
||||
const updateSearchQuery = useDebounceFn((value: string) => {
|
||||
searchQuery.value = value
|
||||
}, SEARCH_DEBOUNCE_DELAY_MS)
|
||||
|
||||
// Watch internal query changes and trigger debounced update
|
||||
watch(internalSearchQuery, (newValue) => {
|
||||
void updateSearchQuery(newValue)
|
||||
})
|
||||
|
||||
// Sync external changes back to internal state
|
||||
watch(searchQuery, (newValue) => {
|
||||
if (newValue !== internalSearchQuery.value) {
|
||||
internalSearchQuery.value = newValue || ''
|
||||
}
|
||||
})
|
||||
|
||||
const input = ref<{ $el: HTMLElement } | null>()
|
||||
const focusInput = () => {
|
||||
if (input.value && input.value.$el) {
|
||||
input.value.$el.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => autofocus && focusInput())
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const baseClasses =
|
||||
'relative flex w-full items-center gap-2 bg-secondary-background cursor-text'
|
||||
|
||||
if (showBorder) {
|
||||
return cn(
|
||||
baseClasses,
|
||||
'rounded p-2 border border-solid border-border-default'
|
||||
)
|
||||
}
|
||||
|
||||
// Size-specific classes matching button sizes for consistency
|
||||
const sizeClasses = {
|
||||
md: 'h-8 px-2 py-1.5', // Matches button sm size
|
||||
lg: 'h-10 px-4 py-2' // Matches button md size
|
||||
}[size]
|
||||
|
||||
return cn(baseClasses, 'rounded-lg', sizeClasses)
|
||||
})
|
||||
</script>
|
||||
@@ -23,6 +23,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #tool-buttons>
|
||||
<!-- Normal Tab View -->
|
||||
<TabList v-if="!isInFolderView" v-model="activeTab">
|
||||
<Tab class="font-inter" value="output">{{
|
||||
$t('sideToolbar.labels.generated')
|
||||
}}</Tab>
|
||||
<Tab class="font-inter" value="input">{{
|
||||
$t('sideToolbar.labels.imported')
|
||||
}}</Tab>
|
||||
</TabList>
|
||||
</template>
|
||||
<template #header>
|
||||
<!-- Job Detail View Header -->
|
||||
<div v-if="isInFolderView" class="px-2 2xl:px-4">
|
||||
@@ -36,15 +47,7 @@
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
<!-- Normal Tab View -->
|
||||
<TabList v-else v-model="activeTab" class="font-inter px-2 2xl:px-4">
|
||||
<Tab class="font-inter" value="output">{{
|
||||
$t('sideToolbar.labels.generated')
|
||||
}}</Tab>
|
||||
<Tab class="font-inter" value="input">{{
|
||||
$t('sideToolbar.labels.imported')
|
||||
}}</Tab>
|
||||
</TabList>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<MediaAssetFilterBar
|
||||
v-model:search-query="searchQuery"
|
||||
@@ -55,12 +58,11 @@
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading">
|
||||
<Divider type="dashed" class="m-2" />
|
||||
<div v-if="loading && !displayAssets.length">
|
||||
<ProgressSpinner class="absolute left-1/2 w-[50px] -translate-x-1/2" />
|
||||
</div>
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="!displayAssets.length">
|
||||
<div v-else-if="!loading && !displayAssets.length">
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
@@ -73,7 +75,6 @@
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div v-else class="relative size-full" @click="handleEmptySpaceClick">
|
||||
<VirtualGrid
|
||||
:items="mediaAssetsWithKey"
|
||||
@@ -174,6 +175,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||
import { Divider } from 'primevue'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
@@ -17,17 +17,19 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="model-lib-search-box p-2 2xl:p-4"
|
||||
:placeholder="$t('g.searchModels') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<div class="px-2 2xl:px-4">
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
:placeholder="$t('g.searchModels') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<ElectronDownloadItems v-if="isElectron()" />
|
||||
|
||||
<Divider type="dashed" class="m-2" />
|
||||
<TreeExplorer
|
||||
v-model:expanded-keys="expandedKeys"
|
||||
class="model-lib-tree-explorer"
|
||||
@@ -43,6 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Divider } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, nextTick, onMounted, ref, toRef, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@
|
||||
</Popover>
|
||||
</template>
|
||||
<template #header>
|
||||
<div>
|
||||
<div class="px-2 2xl:px-4">
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="node-lib-search-box p-2 2xl:p-4"
|
||||
class="node-lib-search-box"
|
||||
:placeholder="$t('g.searchNodes') + '...'"
|
||||
filter-icon="pi pi-filter"
|
||||
:filters
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<div class="comfy-vue-side-bar-header flex flex-col gap-2">
|
||||
<Toolbar
|
||||
class="min-h-15.5 bg-transparent rounded-none border-x-0 border-t-0 px-2 2xl:px-4"
|
||||
class="min-h-16 bg-transparent rounded-none border-x-0 border-t-0 px-2 2xl:px-4"
|
||||
>
|
||||
<template #start>
|
||||
<span class="truncate font-bold" :title="props.title">
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="workflows-search-box p-2 2xl:p-4"
|
||||
:placeholder="$t('g.searchWorkflows') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<div class="px-2 2xl:px-4">
|
||||
<SearchBox
|
||||
ref="searchBoxRef"
|
||||
v-model:model-value="searchQuery"
|
||||
class="workflows-search-box"
|
||||
:placeholder="$t('g.searchWorkflows') + '...'"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="!isSearching" class="comfyui-workflows-panel">
|
||||
|
||||
@@ -33,7 +33,7 @@ const toast = useToast()
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const migrateToLitegraphReroute = async () => {
|
||||
const workflowJSON = app.graph.serialize() as unknown as WorkflowJSON04
|
||||
const workflowJSON = app.rootGraph.serialize() as unknown as WorkflowJSON04
|
||||
const migratedWorkflowJSON = migrateLegacyRerouteNodes(workflowJSON)
|
||||
await app.loadGraphData(
|
||||
migratedWorkflowJSON,
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<Toast
|
||||
group="vue-nodes-migration"
|
||||
position="bottom-center"
|
||||
class="w-auto"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #message>
|
||||
<div class="flex flex-auto items-center justify-between gap-4">
|
||||
<span class="whitespace-nowrap">{{
|
||||
t('vueNodesMigration.message')
|
||||
}}</span>
|
||||
<Button
|
||||
class="whitespace-nowrap"
|
||||
size="small"
|
||||
:label="t('vueNodesMigration.button')"
|
||||
text
|
||||
@click="switchBack"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Toast>
|
||||
<Toast
|
||||
group="vue-nodes-check-main-menu"
|
||||
position="bottom-center"
|
||||
class="w-auto"
|
||||
>
|
||||
<template #message>
|
||||
<div class="flex flex-auto items-center justify-between gap-4">
|
||||
<span class="whitespace-nowrap">{{
|
||||
t('vueNodesMigrationMainMenu.message')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Toast>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'primevue'
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useVueNodesMigrationDismissed } from '@/composables/useVueNodesMigrationDismissed'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const isDismissed = useVueNodesMigrationDismissed()
|
||||
|
||||
const switchBack = async () => {
|
||||
await disableVueNodes()
|
||||
toast.removeGroup('vue-nodes-migration')
|
||||
isDismissed.value = true
|
||||
showMainMenuToast()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
isDismissed.value = true
|
||||
showMainMenuToast()
|
||||
}
|
||||
|
||||
const disableVueNodes = async () => {
|
||||
await useSettingStore().set('Comfy.VueNodes.Enabled', false)
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: `vue_nodes_migration_toast_switch_back_clicked`
|
||||
})
|
||||
}
|
||||
|
||||
const showMainMenuToast = () => {
|
||||
useToastStore().add({
|
||||
group: 'vue-nodes-check-main-menu',
|
||||
severity: 'info',
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -248,7 +248,7 @@ describe('CurrentUserPopover', () => {
|
||||
|
||||
// Verify window.open was called with the correct URL
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
|
||||
'https://docs.comfy.org/tutorials/partner-nodes/pricing',
|
||||
'_blank'
|
||||
)
|
||||
|
||||
|
||||
@@ -21,12 +21,21 @@
|
||||
<p v-if="userEmail" class="my-0 truncate text-sm text-muted">
|
||||
{{ userEmail }}
|
||||
</p>
|
||||
<p v-if="subscriptionTierName" class="my-0 truncate text-sm text-muted">
|
||||
{{ subscriptionTierName }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Credits Section -->
|
||||
<div v-if="isActiveSubscription" class="flex items-center gap-2 px-4 py-2">
|
||||
<i class="icon-[lucide--component] text-amber-400 text-sm" />
|
||||
<span class="text-base font-normal text-base-foreground flex-1">{{
|
||||
<Skeleton
|
||||
v-if="authStore.isFetchingBalance"
|
||||
width="4rem"
|
||||
height="1.25rem"
|
||||
class="flex-1"
|
||||
/>
|
||||
<span v-else class="text-base font-normal text-base-foreground flex-1">{{
|
||||
formattedBalance
|
||||
}}</span>
|
||||
<Button
|
||||
@@ -39,14 +48,15 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SubscribeButton
|
||||
v-else
|
||||
class="mx-4"
|
||||
:label="$t('subscription.subscribeToComfyCloud')"
|
||||
size="small"
|
||||
variant="gradient"
|
||||
@subscribed="handleSubscribed"
|
||||
/>
|
||||
<div v-else class="flex justify-center px-4">
|
||||
<SubscribeButton
|
||||
:fluid="false"
|
||||
:label="$t('subscription.subscribeToComfyCloud')"
|
||||
size="small"
|
||||
variant="gradient"
|
||||
@subscribed="handleSubscribed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Credits info row -->
|
||||
<div
|
||||
@@ -121,6 +131,7 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -152,7 +163,8 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const dialogService = useDialogService()
|
||||
const { isActiveSubscription, fetchStatus } = useSubscription()
|
||||
const { isActiveSubscription, subscriptionTierName, fetchStatus } =
|
||||
useSubscription()
|
||||
const { flags } = useFeatureFlags()
|
||||
const { locale } = useI18n()
|
||||
|
||||
@@ -193,7 +205,7 @@ const handleTopUp = () => {
|
||||
|
||||
const handleOpenPartnerNodesInfo = () => {
|
||||
window.open(
|
||||
buildDocsUrl('/tutorials/api-nodes/overview#api-nodes', {
|
||||
buildDocsUrl('/tutorials/partner-nodes/pricing', {
|
||||
includeLocale: true
|
||||
}),
|
||||
'_blank'
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="showVueNodesBanner"
|
||||
class="pointer-events-auto relative w-full h-10 bg-gradient-to-r from-blue-600 to-blue-700 flex items-center justify-center px-4"
|
||||
>
|
||||
<div class="flex items-center text-sm text-white">
|
||||
<i class="icon-[lucide--rocket]"></i>
|
||||
<span class="pl-2">{{ $t('vueNodesBanner.title') }}</span>
|
||||
<span class="pl-1.5 hidden md:inline">{{
|
||||
$t('vueNodesBanner.desc')
|
||||
}}</span>
|
||||
<Button
|
||||
class="cursor-pointer bg-transparent rounded h-7 px-3 border border-white text-white ml-4 text-xs"
|
||||
@click="handleTryItOut"
|
||||
>
|
||||
{{ $t('vueNodesBanner.tryItOut') }}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
class="cursor-pointer bg-transparent border-0 outline-0 grid place-items-center absolute right-4 text-white"
|
||||
unstyled
|
||||
@click="handleDismiss"
|
||||
>
|
||||
<i class="w-5 h-5 icon-[lucide--x]"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const STORAGE_KEY = 'vueNodesBannerDismissed'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const bannerDismissed = useLocalStorage(STORAGE_KEY, false)
|
||||
|
||||
const vueNodesEnabled = computed(() => {
|
||||
try {
|
||||
return settingStore.get('Comfy.VueNodes.Enabled') ?? false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const showVueNodesBanner = computed(() => {
|
||||
if (vueNodesEnabled.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (bannerDismissed.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
const handleDismiss = (): void => {
|
||||
bannerDismissed.value = true
|
||||
}
|
||||
|
||||
const handleTryItOut = async (): Promise<void> => {
|
||||
try {
|
||||
await settingStore.set('Comfy.VueNodes.Enabled', true)
|
||||
} catch (error) {
|
||||
console.error('Failed to enable Nodes 2.0:', error)
|
||||
} finally {
|
||||
handleDismiss()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -140,8 +140,8 @@ import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
|
||||
@@ -9,7 +9,7 @@ import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SearchBox from '@/components/input/SearchBox.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
|
||||
Reference in New Issue
Block a user