mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-20 06:44:32 +00:00
Merge remote-tracking branch 'origin/main' into fix/remove-any-types-part8
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
>
|
||||
<slot name="actions" :node="props.node" />
|
||||
</div>
|
||||
|
||||
@@ -25,13 +25,31 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const shownWidgets = widgets
|
||||
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced)
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
|
||||
return { widgets: shownWidgets, node }
|
||||
})
|
||||
})
|
||||
|
||||
const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const advancedWidgets = widgets
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
return { widgets: advancedWidgets, node }
|
||||
})
|
||||
.filter(({ widgets }) => widgets.length > 0)
|
||||
})
|
||||
|
||||
const isMultipleNodesSelected = computed(
|
||||
() => widgetsSectionDataList.value.length > 1
|
||||
)
|
||||
@@ -56,6 +74,12 @@ const label = computed(() => {
|
||||
: t('rightSidePanel.inputsNone')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
|
||||
const advancedLabel = computed(() => {
|
||||
return !mustShowNodeTitle && !isMultipleNodesSelected.value
|
||||
? t('rightSidePanel.advancedInputs')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,4 +117,16 @@ const label = computed(() => {
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
<SectionWidgets
|
||||
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
||||
:key="`advanced-${node.id}`"
|
||||
:collapse="true"
|
||||
:node
|
||||
:label="advancedLabel"
|
||||
:widgets
|
||||
:show-locate-button="isMultipleNodesSelected"
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -123,8 +123,7 @@ export const useContextMenuTranslation = () => {
|
||||
}
|
||||
|
||||
// for capture translation text of input and widget
|
||||
const extraInfo = (options.extra ||
|
||||
options.parentMenu?.options?.extra) as
|
||||
const extraInfo = (options.extra || options.parentMenu?.options?.extra) as
|
||||
| { inputs?: INodeInputSlot[]; widgets?: IWidget[] }
|
||||
| undefined
|
||||
// widgets and inputs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
@@ -95,6 +96,8 @@ export function useFeatureFlags() {
|
||||
)
|
||||
},
|
||||
get teamWorkspacesEnabled() {
|
||||
if (!isCloud) return false
|
||||
|
||||
return (
|
||||
remoteConfig.value.team_workspaces_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.TEAM_WORKSPACES_ENABLED, false)
|
||||
|
||||
@@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
})
|
||||
|
||||
function inner_clicked(
|
||||
this: ContextMenu<string>,
|
||||
this: ContextMenuDivElement,
|
||||
v?: string | IContextMenuValue<string>
|
||||
) {
|
||||
if (!node || typeof v === 'string' || !v?.value) return
|
||||
|
||||
const rect = this.root.getBoundingClientRect()
|
||||
const rect = this.getBoundingClientRect()
|
||||
canvas.showEditPropertyValue(node, v.value, {
|
||||
position: [rect.left, rect.top]
|
||||
})
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"nodesCount": "{count} nodes | {count} node | {count} nodes",
|
||||
"community": "Community",
|
||||
"all": "All",
|
||||
"versionMismatchWarning": "Version Compatibility Warning",
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
<template>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex gap-3 items-center">
|
||||
<SearchBox
|
||||
:model-value="searchQuery"
|
||||
:placeholder="$t('sideToolbar.searchAssets') + '...'"
|
||||
@update:model-value="handleSearchChange"
|
||||
/>
|
||||
<MediaAssetFilterButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetFilterMenu
|
||||
:media-type-filters="mediaTypeFilters"
|
||||
:close="close"
|
||||
@update:media-type-filters="handleMediaTypeFiltersChange"
|
||||
/>
|
||||
</template>
|
||||
</MediaAssetFilterButton>
|
||||
<AssetSortButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetSortMenu
|
||||
v-model:sort-by="sortBy"
|
||||
:show-generation-time-sort
|
||||
:close="close"
|
||||
/>
|
||||
</template>
|
||||
</AssetSortButton>
|
||||
<MediaAssetViewModeToggle
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
v-model:view-mode="viewMode"
|
||||
/>
|
||||
<div class="flex gap-1.5 items-center">
|
||||
<MediaAssetFilterButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.filterBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetFilterMenu
|
||||
:media-type-filters
|
||||
:close
|
||||
@update:media-type-filters="handleMediaTypeFiltersChange"
|
||||
/>
|
||||
</template>
|
||||
</MediaAssetFilterButton>
|
||||
<AssetSortButton
|
||||
v-if="isCloud"
|
||||
v-tooltip.top="{ value: $t('assetBrowser.sortBy') }"
|
||||
size="md"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetSortMenu
|
||||
v-model:sort-by="sortBy"
|
||||
:show-generation-time-sort
|
||||
:close
|
||||
/>
|
||||
</template>
|
||||
</AssetSortButton>
|
||||
<MediaAssetViewModeToggle
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
v-model:view-mode="viewMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ type Distribution = 'desktop' | 'localhost' | 'cloud'
|
||||
|
||||
declare global {
|
||||
const __DISTRIBUTION__: Distribution
|
||||
const __IS_NIGHTLY__: boolean
|
||||
}
|
||||
|
||||
/** Current distribution - replaced at compile time */
|
||||
@@ -18,3 +19,10 @@ const DISTRIBUTION: Distribution = __DISTRIBUTION__
|
||||
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
|
||||
export const isCloud = DISTRIBUTION === 'cloud'
|
||||
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
|
||||
|
||||
/**
|
||||
* Whether this is a nightly build (from main branch).
|
||||
* Nightly builds may show experimental features and surveys.
|
||||
* @public
|
||||
*/
|
||||
export const isNightly = __IS_NIGHTLY__
|
||||
|
||||
131
src/platform/surveys/useFeatureUsageTracker.test.ts
Normal file
131
src/platform/surveys/useFeatureUsageTracker.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const STORAGE_KEY = 'Comfy.FeatureUsage'
|
||||
|
||||
describe('useFeatureUsageTracker', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('initializes with zero count for new feature', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
expect(useCount.value).toBe(0)
|
||||
})
|
||||
|
||||
it('increments count on trackUsage', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
expect(useCount.value).toBe(0)
|
||||
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(1)
|
||||
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('sets firstUsed only on first use', async () => {
|
||||
vi.useFakeTimers()
|
||||
const firstTs = 1000000
|
||||
vi.setSystemTime(firstTs)
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
expect(usage.value?.firstUsed).toBe(firstTs)
|
||||
|
||||
vi.setSystemTime(firstTs + 5000)
|
||||
trackUsage()
|
||||
expect(usage.value?.firstUsed).toBe(firstTs)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('updates lastUsed on each use', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { usage, trackUsage } = useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
const firstLastUsed = usage.value?.lastUsed ?? 0
|
||||
|
||||
vi.advanceTimersByTime(10)
|
||||
trackUsage()
|
||||
|
||||
expect(usage.value?.lastUsed).toBeGreaterThan(firstLastUsed)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('reset clears feature data', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount, trackUsage, reset } =
|
||||
useFeatureUsageTracker('test-feature')
|
||||
|
||||
trackUsage()
|
||||
trackUsage()
|
||||
expect(useCount.value).toBe(2)
|
||||
|
||||
reset()
|
||||
expect(useCount.value).toBe(0)
|
||||
})
|
||||
|
||||
it('tracks multiple features independently', async () => {
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const featureA = useFeatureUsageTracker('feature-a')
|
||||
const featureB = useFeatureUsageTracker('feature-b')
|
||||
|
||||
featureA.trackUsage()
|
||||
featureA.trackUsage()
|
||||
featureB.trackUsage()
|
||||
|
||||
expect(featureA.useCount.value).toBe(2)
|
||||
expect(featureB.useCount.value).toBe(1)
|
||||
})
|
||||
|
||||
it('persists to localStorage', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { useFeatureUsageTracker } =
|
||||
await import('./useFeatureUsageTracker')
|
||||
const { trackUsage } = useFeatureUsageTracker('persisted-feature')
|
||||
|
||||
trackUsage()
|
||||
await vi.runAllTimersAsync()
|
||||
|
||||
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}')
|
||||
expect(stored['persisted-feature']?.useCount).toBe(1)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('loads existing data from localStorage', async () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
'existing-feature': { useCount: 5, firstUsed: 1000, lastUsed: 2000 }
|
||||
})
|
||||
)
|
||||
|
||||
vi.resetModules()
|
||||
const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker')
|
||||
const { useCount } = useFeatureUsageTracker('existing-feature')
|
||||
|
||||
expect(useCount.value).toBe(5)
|
||||
})
|
||||
})
|
||||
46
src/platform/surveys/useFeatureUsageTracker.ts
Normal file
46
src/platform/surveys/useFeatureUsageTracker.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface FeatureUsage {
|
||||
useCount: number
|
||||
firstUsed: number
|
||||
lastUsed: number
|
||||
}
|
||||
|
||||
type FeatureUsageRecord = Record<string, FeatureUsage>
|
||||
|
||||
const STORAGE_KEY = 'Comfy.FeatureUsage'
|
||||
|
||||
/**
|
||||
* Tracks feature usage for survey eligibility.
|
||||
* Persists to localStorage.
|
||||
*/
|
||||
export function useFeatureUsageTracker(featureId: string) {
|
||||
const usageData = useStorage<FeatureUsageRecord>(STORAGE_KEY, {})
|
||||
|
||||
const usage = computed(() => usageData.value[featureId])
|
||||
|
||||
const useCount = computed(() => usage.value?.useCount ?? 0)
|
||||
|
||||
function trackUsage() {
|
||||
const now = Date.now()
|
||||
const existing = usageData.value[featureId]
|
||||
|
||||
usageData.value[featureId] = {
|
||||
useCount: (existing?.useCount ?? 0) + 1,
|
||||
firstUsed: existing?.firstUsed ?? now,
|
||||
lastUsed: now
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
delete usageData.value[featureId]
|
||||
}
|
||||
|
||||
return {
|
||||
usage,
|
||||
useCount,
|
||||
trackUsage,
|
||||
reset
|
||||
}
|
||||
}
|
||||
@@ -482,18 +482,30 @@ const lgraphNode = computed(() => {
|
||||
|
||||
const showAdvancedInputsButton = computed(() => {
|
||||
const node = lgraphNode.value
|
||||
if (!node || !(node instanceof SubgraphNode)) return false
|
||||
if (!node) return false
|
||||
|
||||
// Check if there are hidden inputs (widgets not promoted)
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
|
||||
// For subgraph nodes: check for unpromoted widgets
|
||||
if (node instanceof SubgraphNode) {
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])
|
||||
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
|
||||
}
|
||||
|
||||
return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)
|
||||
// For regular nodes: show button if there are advanced widgets and they're currently hidden
|
||||
const hasAdvancedWidgets = nodeData.widgets?.some((w) => w.options?.advanced)
|
||||
return hasAdvancedWidgets && !node.showAdvanced
|
||||
})
|
||||
|
||||
function handleShowAdvancedInputs() {
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
rightSidePanelStore.focusSection('advanced-inputs')
|
||||
const node = lgraphNode.value
|
||||
if (!node) return
|
||||
|
||||
if (node instanceof SubgraphNode) {
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
rightSidePanelStore.focusSection('advanced-inputs')
|
||||
} else {
|
||||
node.showAdvanced = true
|
||||
}
|
||||
}
|
||||
|
||||
const nodeMedia = computed(() => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Popover from 'primevue/popover'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ControlOptions } from '@/types/simplifiedWidget'
|
||||
@@ -14,14 +13,8 @@ type ControlOption = {
|
||||
title: string
|
||||
}
|
||||
|
||||
const popover = ref()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
defineExpose({ toggle })
|
||||
|
||||
const controlOptions: ControlOption[] = [
|
||||
{
|
||||
mode: 'fixed',
|
||||
@@ -57,70 +50,63 @@ const controlMode = defineModel<ControlOptions>()
|
||||
</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.valueControl.header.prefix') }}
|
||||
<span class="text-base-foreground font-medium">
|
||||
{{
|
||||
widgetControlMode === 'before'
|
||||
? $t('widgets.valueControl.header.before')
|
||||
: $t('widgets.valueControl.header.after')
|
||||
}}
|
||||
</span>
|
||||
{{ $t('widgets.valueControl.header.postfix') }}
|
||||
</div>
|
||||
<div class="w-113 max-w-md p-4 space-y-4">
|
||||
<div class="text-sm text-muted-foreground leading-tight">
|
||||
{{ $t('widgets.valueControl.header.prefix') }}
|
||||
<span class="text-base-foreground font-medium">
|
||||
{{
|
||||
widgetControlMode === 'before'
|
||||
? $t('widgets.valueControl.header.before')
|
||||
: $t('widgets.valueControl.header.after')
|
||||
}}
|
||||
</span>
|
||||
{{ $t('widgets.valueControl.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"
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
{{ $t(`widgets.valueControl.${option.title}`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-normal text-muted-foreground leading-tight"
|
||||
>
|
||||
{{ $t(`widgets.valueControl.${option.description}`) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ option.text }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<RadioButton
|
||||
v-model="controlMode"
|
||||
class="flex-shrink-0"
|
||||
:input-id="option.mode"
|
||||
:value="option.mode"
|
||||
/>
|
||||
<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>
|
||||
{{ $t(`widgets.valueControl.${option.title}`) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm font-normal text-muted-foreground leading-tight"
|
||||
>
|
||||
{{ $t(`widgets.valueControl.${option.description}`) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioButton
|
||||
v-model="controlMode"
|
||||
class="flex-shrink-0"
|
||||
:input-id="option.mode"
|
||||
:value="option.mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,13 +24,5 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
ref="domEl"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
@mousedown.stop
|
||||
@mousemove.stop
|
||||
@mouseup.stop
|
||||
/>
|
||||
<div ref="domEl" @pointerdown.stop @pointermove.stop @pointerup.stop />
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { onClickOutside } from '@vueuse/core'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -65,7 +66,6 @@ function updateValue(e: UIEvent) {
|
||||
textEdit.value = false
|
||||
}
|
||||
|
||||
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
|
||||
const canDecrement = computed(
|
||||
() =>
|
||||
modelValue.value > filteredProps.value.min &&
|
||||
@@ -205,16 +205,17 @@ const sliderWidth = computed(() => {
|
||||
class="bg-primary-background/15 absolute left-0 bottom-0 h-full rounded-lg pointer-events-none"
|
||||
:style="{ width: `${sliderWidth}%` }"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="decrement"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-minus', !canDecrement && 'opacity-60')
|
||||
"
|
||||
class="h-full w-8 rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
variant="muted-textonly"
|
||||
:disabled="!canDecrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue -= stepValue"
|
||||
/>
|
||||
>
|
||||
<i class="pi pi-minus" />
|
||||
</Button>
|
||||
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
|
||||
<input
|
||||
ref="inputField"
|
||||
@@ -262,16 +263,17 @@ const sliderWidth = computed(() => {
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
<button
|
||||
<Button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="increment"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-plus', !canIncrement && 'opacity-60')
|
||||
"
|
||||
class="h-full w-8 rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
variant="muted-textonly"
|
||||
:disabled="!canIncrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue += stepValue"
|
||||
/>
|
||||
>
|
||||
<i class="pi pi-plus" />
|
||||
</Button>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type {
|
||||
SimplifiedControlWidget,
|
||||
@@ -19,8 +20,6 @@ const props = defineProps<{
|
||||
|
||||
const modelValue = defineModel<T>()
|
||||
|
||||
const popover = ref()
|
||||
|
||||
const controlModel = ref(props.widget.controlWidget.value)
|
||||
|
||||
const controlButtonIcon = computed(() => {
|
||||
@@ -37,24 +36,24 @@ const controlButtonIcon = computed(() => {
|
||||
})
|
||||
|
||||
watch(controlModel, props.widget.controlWidget.update)
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative grid grid-cols-subgrid">
|
||||
<component :is="component" v-bind="$attrs" v-model="modelValue" :widget>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
class="h-4 w-7 self-center rounded-xl bg-blue-100/30 p-0"
|
||||
@click.stop.prevent="togglePopover"
|
||||
>
|
||||
<i :class="`${controlButtonIcon} text-blue-100 text-xs size-3.5`" />
|
||||
</Button>
|
||||
<Popover>
|
||||
<template #button>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
class="h-4 w-7 p-0 self-center rounded-xl bg-primary-background/30 hover:bg-primary-background-hover/30"
|
||||
>
|
||||
<i
|
||||
:class="`${controlButtonIcon} text-primary-background text-xs w-full`"
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<ValueControlPopover v-model="controlModel" />
|
||||
</Popover>
|
||||
</component>
|
||||
<ValueControlPopover ref="popover" v-model="controlModel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,15 +10,22 @@ import type {
|
||||
RegistryPack
|
||||
} from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const translateMock = vi.hoisted(() =>
|
||||
vi.fn((key: string, choice?: number) =>
|
||||
typeof choice === 'number' ? `${key}-${choice}` : key
|
||||
)
|
||||
)
|
||||
const dateMock = vi.hoisted(() => vi.fn(() => '2024. 1. 1.'))
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: vi.fn(() => ({
|
||||
d: vi.fn(() => '2024. 1. 1.'),
|
||||
t: vi.fn((key: string) => key)
|
||||
d: dateMock,
|
||||
t: translateMock
|
||||
})),
|
||||
createI18n: vi.fn(() => ({
|
||||
global: {
|
||||
t: vi.fn((key: string) => key),
|
||||
t: translateMock,
|
||||
te: vi.fn(() => true)
|
||||
}
|
||||
}))
|
||||
@@ -187,6 +194,18 @@ describe('PackCard', () => {
|
||||
// Should still render without errors
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('should use localized singular/plural nodes label', () => {
|
||||
const packWithNodes = {
|
||||
...mockNodePack,
|
||||
comfy_nodes: ['node-a']
|
||||
} as MergedNodePack
|
||||
|
||||
const wrapper = createWrapper({ nodePack: packWithNodes })
|
||||
|
||||
expect(wrapper.text()).toContain('g.nodesCount-1')
|
||||
expect(translateMock).toHaveBeenCalledWith('g.nodesCount', 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('component structure', () => {
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
</p>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<div class="flex flex-1 items-center gap-2">
|
||||
<div v-if="nodesCount" class="p-2 pl-0 text-xs">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
<div v-if="nodesLabel" class="p-2 pl-0 text-xs">
|
||||
{{ nodesLabel }}
|
||||
</div>
|
||||
<PackVersionBadge
|
||||
:node-pack="nodePack"
|
||||
@@ -94,7 +94,7 @@ const { nodePack, isSelected = false } = defineProps<{
|
||||
isSelected?: boolean
|
||||
}>()
|
||||
|
||||
const { d } = useI18n()
|
||||
const { d, t } = useI18n()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const isLightTheme = computed(
|
||||
@@ -115,6 +115,9 @@ const isDisabled = computed(
|
||||
const nodesCount = computed(() =>
|
||||
isMergedNodePack(nodePack) ? nodePack.comfy_nodes?.length : undefined
|
||||
)
|
||||
const nodesLabel = computed(() =>
|
||||
nodesCount.value ? t('g.nodesCount', nodesCount.value) : ''
|
||||
)
|
||||
const publisherName = computed(() => {
|
||||
if (!nodePack) return null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user