mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Compare commits
26 Commits
v1.37.3
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d930514bea | ||
|
|
99cb7a2da1 | ||
|
|
b3d87673ec | ||
|
|
6a733918a7 | ||
|
|
a87d2cf1bd | ||
|
|
a1d689d3b3 | ||
|
|
dc64e16f7c | ||
|
|
c19a004f0d | ||
|
|
626d8dac70 | ||
|
|
b6a12ddae1 | ||
|
|
11f8cdb9bd | ||
|
|
dcf0886d89 | ||
|
|
ab6678534f | ||
|
|
ea3b3ceb00 | ||
|
|
2356b0bc9e | ||
|
|
dad1eafecc | ||
|
|
6e5dfc0109 | ||
|
|
43f0ac2e8f | ||
|
|
76a0b0b4b4 | ||
|
|
e6e93f2ebf | ||
|
|
372890811d | ||
|
|
14d0ec73f6 | ||
|
|
fbdaf5d7f3 | ||
|
|
a7d0825a14 | ||
|
|
10feb1fd5b | ||
|
|
832588c7a9 |
@@ -83,7 +83,7 @@ test.describe('Templates', () => {
|
||||
|
||||
await comfyPage.page
|
||||
.locator(
|
||||
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
|
||||
'nav > div:nth-child(3) > div > span:has-text("Getting Started")'
|
||||
)
|
||||
.click()
|
||||
await comfyPage.templates.loadTemplate('default')
|
||||
|
||||
66
docs/TEMPLATE_RANKING.md
Normal file
66
docs/TEMPLATE_RANKING.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Template Ranking System
|
||||
|
||||
Usage-based ordering for workflow templates with position bias normalization.
|
||||
|
||||
Scores are pre-computed and normalized offline and shipped as static JSON (mirrors `sorted-custom-node-map.json` pattern for node search).
|
||||
|
||||
## Sort Modes
|
||||
|
||||
| Mode | Formula | Description |
|
||||
| -------------- | ------------------------------------------------ | ---------------------- |
|
||||
| `recommended` | `usage × 0.5 + internal × 0.3 + freshness × 0.2` | Curated recommendation |
|
||||
| `popular` | `usage × 0.9 + freshness × 0.1` | Pure user-driven |
|
||||
| `newest` | Date sort | Existing |
|
||||
| `alphabetical` | Name sort | Existing |
|
||||
|
||||
Freshness computed at runtime from `template.date`: `1.0 / (1 + daysSinceAdded / 90)`, min 0.1.
|
||||
|
||||
## Data Files
|
||||
|
||||
**Usage scores** (generated from Mixpanel):
|
||||
|
||||
```json
|
||||
// In templates/index.json, add to any template:
|
||||
{
|
||||
"name": "some_template",
|
||||
"usage": 1000,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Search rank** (set per-template in workflow_templates repo):
|
||||
|
||||
```json
|
||||
// In templates/index.json, add to any template:
|
||||
{
|
||||
"name": "some_template",
|
||||
"searchRank": 8, // Scale 1-10, default 5
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
| searchRank | Effect |
|
||||
| ---------- | ---------------------------- |
|
||||
| 1-4 | Demote (bury in results) |
|
||||
| 5 | Neutral (default if not set) |
|
||||
| 6-10 | Promote (boost in results) |
|
||||
|
||||
## Position Bias Correction
|
||||
|
||||
Raw usage reflects true preference AND UI position bias. We use linear interpolation:
|
||||
|
||||
```
|
||||
correction = 1 + (position - 1) / (maxPosition - 1)
|
||||
normalizedUsage = rawUsage × correction
|
||||
```
|
||||
|
||||
| Position | Boost |
|
||||
| -------- | ----- |
|
||||
| 1 | 1.0× |
|
||||
| 50 | 1.28× |
|
||||
| 100 | 1.57× |
|
||||
| 175 | 2.0× |
|
||||
|
||||
Templates buried at the bottom get up to 2× boost to compensate for reduced visibility.
|
||||
|
||||
---
|
||||
@@ -12,12 +12,17 @@ Documentation for unit tests is organized into three guides:
|
||||
|
||||
## Testing Structure
|
||||
|
||||
The ComfyUI Frontend project uses a mixed approach to unit test organization:
|
||||
The ComfyUI Frontend project uses **colocated tests** - test files are placed alongside their source files:
|
||||
|
||||
- **Component Tests**: Located directly alongside their components with a `.spec.ts` extension
|
||||
- **Unit Tests**: Located in the `tests-ui/tests/` directory
|
||||
- **Store Tests**: Located in the `tests-ui/tests/store/` directory
|
||||
- **Browser Tests**: These are located in the `browser_tests/` directory. There is a dedicated README in the `browser_tests/` directory, so it will not be covered here.
|
||||
- **Component Tests**: Located directly alongside their components (e.g., `MyComponent.test.ts` next to `MyComponent.vue`)
|
||||
- **Unit Tests**: Located alongside their source files (e.g., `myUtil.test.ts` next to `myUtil.ts`)
|
||||
- **Store Tests**: Located in `src/stores/` alongside their store files
|
||||
- **Browser Tests**: Located in the `browser_tests/` directory (see dedicated README there)
|
||||
|
||||
### Test File Naming
|
||||
|
||||
- Use `.test.ts` extension for test files
|
||||
- Name tests after their source file: `sourceFile.test.ts`
|
||||
|
||||
## Test Frameworks and Libraries
|
||||
|
||||
@@ -35,8 +40,11 @@ To run the tests locally:
|
||||
# Run unit tests
|
||||
pnpm test:unit
|
||||
|
||||
# Run a specific test file
|
||||
pnpm test:unit -- src/path/to/file.test.ts
|
||||
|
||||
# Run unit tests in watch mode
|
||||
pnpm test:unit -- --watch
|
||||
```
|
||||
|
||||
Refer to the specific guides for more detailed information on each testing type.
|
||||
Refer to the specific guides for more detailed information on each testing type.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.37.3",
|
||||
"version": "1.37.5",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
|
||||
@custom-variant touch (@media (hover: none));
|
||||
|
||||
@theme {
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getMediaTypeFromFilename, truncateFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilename, truncateFilename } from './formatUtil'
|
||||
|
||||
describe('formatUtil', () => {
|
||||
describe('truncateFilename', () => {
|
||||
@@ -2,7 +2,8 @@
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg bg-secondary-background shadow-sm transition-all duration-200 cursor-pointer'
|
||||
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg shadow-sm transition-all duration-200 cursor-pointer',
|
||||
backgroundClass || 'bg-secondary-background'
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -12,4 +13,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { backgroundClass } = defineProps<{
|
||||
backgroundClass?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="node-actions motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
>
|
||||
<slot name="actions" :node="props.node" />
|
||||
</div>
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
<!-- Actual Template Cards -->
|
||||
<CardContainer
|
||||
v-for="template in isLoading ? [] : displayTemplates"
|
||||
v-show="isTemplateVisibleOnDistribution(template)"
|
||||
:key="template.name"
|
||||
ref="cardRefs"
|
||||
size="compact"
|
||||
@@ -405,6 +406,8 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'
|
||||
import { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
import { createGridStyle } from '@/utils/gridUtil'
|
||||
@@ -423,6 +426,30 @@ onMounted(() => {
|
||||
sessionStartTime.value = Date.now()
|
||||
})
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const distributions = computed(() => {
|
||||
// eslint-disable-next-line no-undef
|
||||
switch (__DISTRIBUTION__) {
|
||||
case 'cloud':
|
||||
return [TemplateIncludeOnDistributionEnum.Cloud]
|
||||
case 'localhost':
|
||||
return [TemplateIncludeOnDistributionEnum.Local]
|
||||
case 'desktop':
|
||||
default:
|
||||
if (systemStatsStore.systemStats?.system.os === 'darwin') {
|
||||
return [
|
||||
TemplateIncludeOnDistributionEnum.Desktop,
|
||||
TemplateIncludeOnDistributionEnum.Mac
|
||||
]
|
||||
}
|
||||
return [
|
||||
TemplateIncludeOnDistributionEnum.Desktop,
|
||||
TemplateIncludeOnDistributionEnum.Windows
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// Wrap onClose to track session end
|
||||
const onClose = () => {
|
||||
if (isCloud) {
|
||||
@@ -511,6 +538,9 @@ const allTemplates = computed(() => {
|
||||
return workflowTemplatesStore.enhancedTemplates
|
||||
})
|
||||
|
||||
// Navigation
|
||||
const selectedNavItem = ref<string | null>('all')
|
||||
|
||||
// Filter templates based on selected navigation item
|
||||
const navigationFilteredTemplates = computed(() => {
|
||||
if (!selectedNavItem.value) {
|
||||
@@ -536,6 +566,36 @@ const {
|
||||
resetFilters
|
||||
} = useTemplateFiltering(navigationFilteredTemplates)
|
||||
|
||||
/**
|
||||
* Coordinates state between the selected navigation item and the sort order to
|
||||
* create deterministic, predictable behavior.
|
||||
* @param source The origin of the change ('nav' or 'sort').
|
||||
*/
|
||||
const coordinateNavAndSort = (source: 'nav' | 'sort') => {
|
||||
const isPopularNav = selectedNavItem.value === 'popular'
|
||||
const isPopularSort = sortBy.value === 'popular'
|
||||
|
||||
if (source === 'nav') {
|
||||
if (isPopularNav && !isPopularSort) {
|
||||
// When navigating to 'Popular' category, automatically set sort to 'Popular'.
|
||||
sortBy.value = 'popular'
|
||||
} else if (!isPopularNav && isPopularSort) {
|
||||
// When navigating away from 'Popular' category while sort is 'Popular', reset sort to default.
|
||||
sortBy.value = 'default'
|
||||
}
|
||||
} else if (source === 'sort') {
|
||||
// When sort is changed away from 'Popular' while in the 'Popular' category,
|
||||
// reset the category to 'All Templates' to avoid a confusing state.
|
||||
if (isPopularNav && !isPopularSort) {
|
||||
selectedNavItem.value = 'all'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes from the two sources ('nav' and 'sort') and trigger the coordinator.
|
||||
watch(selectedNavItem, () => coordinateNavAndSort('nav'))
|
||||
watch(sortBy, () => coordinateNavAndSort('sort'))
|
||||
|
||||
// Convert between string array and object array for MultiSelect component
|
||||
const selectedModelObjects = computed({
|
||||
get() {
|
||||
@@ -578,9 +638,6 @@ const cardRefs = ref<HTMLElement[]>([])
|
||||
// Force re-render key for templates when sorting changes
|
||||
const templateListKey = ref(0)
|
||||
|
||||
// Navigation
|
||||
const selectedNavItem = ref<string | null>('all')
|
||||
|
||||
// Search text for model filter
|
||||
const modelSearchText = ref<string>('')
|
||||
|
||||
@@ -645,11 +702,19 @@ const runsOnFilterLabel = computed(() => {
|
||||
|
||||
// Sort options
|
||||
const sortOptions = computed(() => [
|
||||
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
|
||||
{
|
||||
name: t('templateWorkflows.sort.default', 'Default'),
|
||||
value: 'default'
|
||||
},
|
||||
{
|
||||
name: t('templateWorkflows.sort.recommended', 'Recommended'),
|
||||
value: 'recommended'
|
||||
},
|
||||
{
|
||||
name: t('templateWorkflows.sort.popular', 'Popular'),
|
||||
value: 'popular'
|
||||
},
|
||||
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
|
||||
{
|
||||
name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Usage (Low to High)'),
|
||||
value: 'vram-low-to-high'
|
||||
@@ -750,7 +815,7 @@ const pageTitle = computed(() => {
|
||||
// Initialize templates loading with useAsyncState
|
||||
const { isLoading } = useAsyncState(
|
||||
async () => {
|
||||
// Run both operations in parallel for better performance
|
||||
// Run all operations in parallel for better performance
|
||||
await Promise.all([
|
||||
loadTemplates(),
|
||||
workflowTemplatesStore.loadWorkflowTemplates()
|
||||
@@ -763,6 +828,14 @@ const { isLoading } = useAsyncState(
|
||||
}
|
||||
)
|
||||
|
||||
const isTemplateVisibleOnDistribution = (template: TemplateInfo) => {
|
||||
return (template.includeOnDistributions?.length ?? 0) > 0
|
||||
? distributions.value.some((d) =>
|
||||
template.includeOnDistributions?.includes(d)
|
||||
)
|
||||
: true
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cardRefs.value = [] // Release DOM refs
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<!-- OSS mode: Open Manager + Install All buttons -->
|
||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
||||
<Button variant="textonly" size="sm" @click="openManager">{{
|
||||
<Button variant="textonly" @click="openManager">{{
|
||||
$t('g.openManager')
|
||||
}}</Button>
|
||||
<PackInstallButton
|
||||
|
||||
@@ -49,25 +49,66 @@
|
||||
@select="selectedCredits = option.credits"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground w-96">
|
||||
{{ $t('credits.topUp.templateNote') }}
|
||||
<div class="flex flex-row items-center gap-2 group pt-2">
|
||||
<i
|
||||
class="pi pi-question-circle text-xs text-muted-foreground group-hover:text-base-foreground"
|
||||
/>
|
||||
<span
|
||||
class="text-sm font-normal text-muted-foreground cursor-pointer group-hover:text-base-foreground"
|
||||
@click="togglePopover"
|
||||
>
|
||||
{{ t('subscription.videoTemplateBasedCredits') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buy Button -->
|
||||
<Button
|
||||
:disabled="!selectedCredits || loading"
|
||||
:loading="loading"
|
||||
variant="primary"
|
||||
:class="cn('w-full', (!selectedCredits || loading) && 'opacity-30')"
|
||||
@click="handleBuy"
|
||||
<!-- Buy Button -->
|
||||
<Button
|
||||
:disabled="!selectedCredits || loading"
|
||||
:loading="loading"
|
||||
variant="primary"
|
||||
:class="cn('w-full', (!selectedCredits || loading) && 'opacity-30')"
|
||||
@click="handleBuy"
|
||||
>
|
||||
{{ $t('credits.topUp.buy') }}
|
||||
</Button>
|
||||
</div>
|
||||
<Popover
|
||||
ref="popover"
|
||||
append-to="body"
|
||||
:auto-z-index="true"
|
||||
:base-z-index="1000"
|
||||
:dismissable="true"
|
||||
:close-on-escape="true"
|
||||
unstyled
|
||||
:pt="{
|
||||
root: {
|
||||
class:
|
||||
'rounded-lg border border-interface-stroke bg-interface-panel-surface shadow-lg p-4 max-w-xs'
|
||||
}
|
||||
}"
|
||||
>
|
||||
{{ $t('credits.topUp.buy') }}
|
||||
</Button>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm text-base-foreground leading-normal">
|
||||
{{ t('subscription.videoEstimateExplanation') }}
|
||||
</p>
|
||||
<a
|
||||
href="https://cloud.comfy.org/?template=video_wan2_2_14B_fun_camera"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-azure-600 hover:text-azure-400 no-underline flex gap-1"
|
||||
>
|
||||
<span class="underline">
|
||||
{{ t('subscription.videoEstimateTryTemplate') }}
|
||||
</span>
|
||||
<span class="no-underline" v-html="'→'"></span>
|
||||
</a>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Popover } from 'primevue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -101,22 +142,28 @@ const toast = useToast()
|
||||
const selectedCredits = ref<number | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const popover = ref()
|
||||
|
||||
const togglePopover = (event: Event) => {
|
||||
popover.value.toggle(event)
|
||||
}
|
||||
|
||||
const creditOptions: CreditOption[] = [
|
||||
{
|
||||
credits: 1055, // $5.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 41 })
|
||||
description: t('credits.topUp.videosEstimate', { count: 30 })
|
||||
},
|
||||
{
|
||||
credits: 2110, // $10.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 82 })
|
||||
description: t('credits.topUp.videosEstimate', { count: 60 })
|
||||
},
|
||||
{
|
||||
credits: 4220, // $20.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 184 })
|
||||
description: t('credits.topUp.videosEstimate', { count: 120 })
|
||||
},
|
||||
{
|
||||
credits: 10550, // $50.00
|
||||
description: t('credits.topUp.videosEstimate', { count: 412 })
|
||||
description: t('credits.topUp.videosEstimate', { count: 301 })
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
v-model:light-config="lightConfig"
|
||||
:is-splat-model="isSplatModel"
|
||||
:is-ply-model="isPlyModel"
|
||||
:has-skeleton="hasSkeleton"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
@@ -116,6 +117,7 @@ const {
|
||||
isPreview,
|
||||
isSplatModel,
|
||||
isPlyModel,
|
||||
hasSkeleton,
|
||||
hasRecording,
|
||||
recordingDuration,
|
||||
animations,
|
||||
|
||||
@@ -58,8 +58,10 @@
|
||||
v-if="showModelControls"
|
||||
v-model:material-mode="modelConfig!.materialMode"
|
||||
v-model:up-direction="modelConfig!.upDirection"
|
||||
v-model:show-skeleton="modelConfig!.showSkeleton"
|
||||
:hide-material-mode="isSplatModel"
|
||||
:is-ply-model="isPlyModel"
|
||||
:has-skeleton="hasSkeleton"
|
||||
/>
|
||||
|
||||
<CameraControls
|
||||
@@ -99,9 +101,14 @@ import type {
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { isSplatModel = false, isPlyModel = false } = defineProps<{
|
||||
const {
|
||||
isSplatModel = false,
|
||||
isPlyModel = false,
|
||||
hasSkeleton = false
|
||||
} = defineProps<{
|
||||
isSplatModel?: boolean
|
||||
isPlyModel?: boolean
|
||||
hasSkeleton?: boolean
|
||||
}>()
|
||||
|
||||
const sceneConfig = defineModel<SceneConfig>('sceneConfig')
|
||||
|
||||
@@ -70,6 +70,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasSkeleton">
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: t('load3d.showSkeleton'),
|
||||
showDelay: 300
|
||||
}"
|
||||
size="icon"
|
||||
variant="textonly"
|
||||
:class="cn('rounded-full', showSkeleton && 'bg-blue-500')"
|
||||
:aria-label="t('load3d.showSkeleton')"
|
||||
@click="showSkeleton = !showSkeleton"
|
||||
>
|
||||
<i class="pi pi-sitemap text-lg text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -84,13 +100,19 @@ import type {
|
||||
import { t } from '@/i18n'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { hideMaterialMode = false, isPlyModel = false } = defineProps<{
|
||||
const {
|
||||
hideMaterialMode = false,
|
||||
isPlyModel = false,
|
||||
hasSkeleton = false
|
||||
} = defineProps<{
|
||||
hideMaterialMode?: boolean
|
||||
isPlyModel?: boolean
|
||||
hasSkeleton?: boolean
|
||||
}>()
|
||||
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
const showSkeleton = defineModel<boolean>('showSkeleton')
|
||||
|
||||
const showUpDirection = ref(false)
|
||||
const showMaterialMode = ref(false)
|
||||
|
||||
@@ -22,7 +22,6 @@ const QueueJobItemStub = defineComponent({
|
||||
runningNodeName: { type: String, default: undefined },
|
||||
activeDetailsId: { type: String, default: null }
|
||||
},
|
||||
emits: ['cancel', 'delete', 'menu', 'view', 'details-enter', 'details-leave'],
|
||||
template: '<div class="queue-job-item-stub"></div>'
|
||||
})
|
||||
|
||||
@@ -47,11 +47,36 @@
|
||||
<MediaAssetFilterBar
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:sort-by="sortBy"
|
||||
v-model:view-mode="viewMode"
|
||||
v-model:media-type-filters="mediaTypeFilters"
|
||||
class="pb-1 px-2 2xl:px-4"
|
||||
:show-generation-time-sort="activeTab === 'output'"
|
||||
/>
|
||||
<Divider type="dashed" class="my-2" />
|
||||
<div
|
||||
v-if="isQueuePanelV2Enabled"
|
||||
class="flex items-center justify-between px-2 py-2 2xl:px-4"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ activeJobsLabel }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-base-foreground">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearQueueTooltip') }}
|
||||
</span>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.clearQueueTooltip')
|
||||
"
|
||||
:disabled="queuedCount === 0"
|
||||
@click="handleClearQueue"
|
||||
>
|
||||
<i class="icon-[lucide--list-x] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Divider v-else type="dashed" class="my-2" />
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="loading && !displayAssets.length">
|
||||
@@ -164,7 +189,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||
import { Divider } from 'primevue'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
@@ -187,17 +212,26 @@ import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAs
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { ResultItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t, n } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('output')
|
||||
const folderPromptId = ref<string | null>(null)
|
||||
const folderExecutionTime = ref<number | undefined>(undefined)
|
||||
const isInFolderView = computed(() => folderPromptId.value !== null)
|
||||
const viewMode = ref<'list' | 'grid'>('grid')
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
|
||||
// Track which asset's context menu is open (for single-instance context menu management)
|
||||
const openContextMenuId = ref<string | null>(null)
|
||||
@@ -226,6 +260,19 @@ const formattedExecutionTime = computed(() => {
|
||||
return formatDuration(folderExecutionTime.value * 1000)
|
||||
})
|
||||
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const activeJobsCount = computed(
|
||||
() => queueStore.pendingTasks.length + queueStore.runningTasks.length
|
||||
)
|
||||
const activeJobsLabel = computed(() => {
|
||||
const count = activeJobsCount.value
|
||||
return t(
|
||||
'sideToolbar.queueProgressOverlay.activeJobs',
|
||||
{ count: n(count) },
|
||||
count
|
||||
)
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const inputAssets = useMediaAssets('input')
|
||||
@@ -490,6 +537,10 @@ const handleDeleteSelected = async () => {
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
const handleClearQueue = async () => {
|
||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||
}
|
||||
|
||||
const handleApproachEnd = useDebounceFn(async () => {
|
||||
if (
|
||||
activeTab.value === 'output' &&
|
||||
|
||||
@@ -18,7 +18,8 @@ export const buttonVariants = cva({
|
||||
'muted-textonly':
|
||||
'text-muted-foreground bg-transparent hover:bg-secondary-background-hover',
|
||||
'destructive-textonly':
|
||||
'text-destructive-background bg-transparent hover:bg-destructive-background/10'
|
||||
'text-destructive-background bg-transparent hover:bg-destructive-background/10',
|
||||
'overlay-white': 'bg-white text-gray-600 hover:bg-white/90'
|
||||
},
|
||||
size: {
|
||||
sm: 'h-6 rounded-sm px-2 py-1 text-xs',
|
||||
@@ -44,7 +45,8 @@ const variants = [
|
||||
'destructive',
|
||||
'textonly',
|
||||
'muted-textonly',
|
||||
'destructive-textonly'
|
||||
'destructive-textonly',
|
||||
'overlay-white'
|
||||
] as const satisfies Array<ButtonVariants['variant']>
|
||||
const sizes = ['sm', 'md', 'lg', 'icon', 'icon-sm'] as const satisfies Array<
|
||||
ButtonVariants['size']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<i :class="icon" class="text-neutral text-sm" />
|
||||
<i :class="icon" class="text-neutral text-sm shrink-0" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
|
||||
class="flex cursor-pointer items-start gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
|
||||
:class="
|
||||
active
|
||||
? 'bg-interface-menu-component-surface-selected'
|
||||
@@ -9,9 +9,11 @@
|
||||
role="button"
|
||||
@click="onClick"
|
||||
>
|
||||
<NavIcon v-if="icon" :icon="icon" />
|
||||
<i v-else class="text-neutral icon-[lucide--folder] text-xs" />
|
||||
<span class="flex items-center">
|
||||
<div v-if="icon" class="py-0.5">
|
||||
<NavIcon :icon="icon" />
|
||||
</div>
|
||||
<i v-else class="text-neutral icon-[lucide--folder] text-xs shrink-0" />
|
||||
<span class="flex items-center break-all">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,8 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
type Positionable,
|
||||
Reroute
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { subgraphTest } from '../../litegraph/subgraph/fixtures/subgraphFixtures'
|
||||
import { subgraphTest } from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphFixtures'
|
||||
|
||||
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
||||
|
||||
@@ -284,7 +284,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
await nextTick()
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'report-error')
|
||||
entry?.onClick?.()
|
||||
void entry?.onClick?.()
|
||||
|
||||
expect(dialogServiceMock.showExecutionErrorDialog).toHaveBeenCalledWith(
|
||||
error
|
||||
@@ -460,7 +460,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
await nextTick()
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'download')
|
||||
entry?.onClick?.()
|
||||
void entry?.onClick?.()
|
||||
|
||||
expect(downloadFileMock).toHaveBeenCalledWith('https://asset')
|
||||
})
|
||||
@@ -471,7 +471,7 @@ describe('useJobMenu', () => {
|
||||
|
||||
await nextTick()
|
||||
const entry = findActionEntry(jobMenuEntries.value, 'download')
|
||||
entry?.onClick?.()
|
||||
void entry?.onClick?.()
|
||||
|
||||
expect(downloadFileMock).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -9,8 +9,8 @@ import { app } from '@/scripts/app'
|
||||
|
||||
// Mock vue-i18n for useExternalLink
|
||||
const mockLocale = ref('en')
|
||||
vi.mock('vue-i18n', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('vue-i18n')>()
|
||||
vi.mock('vue-i18n', async () => {
|
||||
const actual = await vi.importActual('vue-i18n')
|
||||
return {
|
||||
...actual,
|
||||
useI18n: vi.fn(() => ({
|
||||
@@ -67,7 +67,6 @@ import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelector
|
||||
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
|
||||
|
||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
|
||||
export function useCoreCommands(): ComfyCommand[] {
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -79,6 +78,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
const executionStore = useExecutionStore()
|
||||
const telemetry = useTelemetry()
|
||||
const { staticUrls, buildDocsUrl } = useExternalLink()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const bottomPanelStore = useBottomPanelStore()
|
||||
|
||||
@@ -86,6 +86,14 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
useSelectedLiteGraphItems()
|
||||
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
|
||||
|
||||
function isQueuePanelV2Enabled() {
|
||||
return settingStore.get('Comfy.Queue.QPOV2')
|
||||
}
|
||||
|
||||
async function toggleQueuePanelV2() {
|
||||
await settingStore.set('Comfy.Queue.QPOV2', !isQueuePanelV2Enabled())
|
||||
}
|
||||
|
||||
const moveSelectedNodes = (
|
||||
positionUpdater: (pos: Point, gridSize: number) => Point
|
||||
) => {
|
||||
@@ -1191,6 +1199,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
await useWorkflowService().reloadCurrentWorkflow() // ensure changes take effect immediately
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleQPOV2',
|
||||
icon: 'pi pi-list',
|
||||
label: 'Toggle Queue Panel V2',
|
||||
function: toggleQueuePanelV2
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleLinear',
|
||||
icon: 'pi pi-database',
|
||||
|
||||
@@ -11,10 +11,12 @@ export enum ServerFeatureFlag {
|
||||
MAX_UPLOAD_SIZE = 'max_upload_size',
|
||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
|
||||
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
|
||||
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
|
||||
ASSET_DELETION_ENABLED = 'asset_deletion_enabled',
|
||||
ASSET_RENAME_ENABLED = 'asset_rename_enabled',
|
||||
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
|
||||
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled',
|
||||
HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled'
|
||||
HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled',
|
||||
ASYNC_MODEL_UPLOAD_ENABLED = 'async_model_upload_enabled'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,14 +43,16 @@ export function useFeatureFlags() {
|
||||
)
|
||||
)
|
||||
},
|
||||
get assetUpdateOptionsEnabled() {
|
||||
// Check remote config first (from /api/features), fall back to websocket feature flags
|
||||
get assetDeletionEnabled() {
|
||||
return (
|
||||
remoteConfig.value.asset_update_options_enabled ??
|
||||
api.getServerFeature(
|
||||
ServerFeatureFlag.ASSET_UPDATE_OPTIONS_ENABLED,
|
||||
false
|
||||
)
|
||||
remoteConfig.value.asset_deletion_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.ASSET_DELETION_ENABLED, false)
|
||||
)
|
||||
},
|
||||
get assetRenameEnabled() {
|
||||
return (
|
||||
remoteConfig.value.asset_rename_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.ASSET_RENAME_ENABLED, false)
|
||||
)
|
||||
},
|
||||
get privateModelsEnabled() {
|
||||
@@ -65,7 +69,6 @@ export function useFeatureFlags() {
|
||||
)
|
||||
},
|
||||
get huggingfaceModelImportEnabled() {
|
||||
// Check remote config first (from /api/features), fall back to websocket feature flags
|
||||
return (
|
||||
remoteConfig.value.huggingface_model_import_enabled ??
|
||||
api.getServerFeature(
|
||||
@@ -73,6 +76,15 @@ export function useFeatureFlags() {
|
||||
false
|
||||
)
|
||||
)
|
||||
},
|
||||
get asyncModelUploadEnabled() {
|
||||
return (
|
||||
remoteConfig.value.async_model_upload_enabled ??
|
||||
api.getServerFeature(
|
||||
ServerFeatureFlag.ASYNC_MODEL_UPLOAD_ENABLED,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ describe('useLoad3d', () => {
|
||||
},
|
||||
'Model Config': {
|
||||
upDirection: 'original',
|
||||
materialMode: 'original'
|
||||
materialMode: 'original',
|
||||
showSkeleton: false
|
||||
},
|
||||
'Camera Config': {
|
||||
cameraType: 'perspective',
|
||||
@@ -107,6 +108,8 @@ describe('useLoad3d', () => {
|
||||
exportModel: vi.fn().mockResolvedValue(undefined),
|
||||
isSplatModel: vi.fn().mockReturnValue(false),
|
||||
isPlyModel: vi.fn().mockReturnValue(false),
|
||||
hasSkeleton: vi.fn().mockReturnValue(false),
|
||||
setShowSkeleton: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
@@ -143,7 +146,8 @@ describe('useLoad3d', () => {
|
||||
})
|
||||
expect(composable.modelConfig.value).toEqual({
|
||||
upDirection: 'original',
|
||||
materialMode: 'original'
|
||||
materialMode: 'original',
|
||||
showSkeleton: false
|
||||
})
|
||||
expect(composable.cameraConfig.value).toEqual({
|
||||
cameraType: 'perspective',
|
||||
@@ -410,7 +414,8 @@ describe('useLoad3d', () => {
|
||||
expect(mockLoad3d.setMaterialMode).toHaveBeenCalledWith('wireframe')
|
||||
expect(mockNode.properties['Model Config']).toEqual({
|
||||
upDirection: '+y',
|
||||
materialMode: 'wireframe'
|
||||
materialMode: 'wireframe',
|
||||
showSkeleton: false
|
||||
})
|
||||
})
|
||||
|
||||
@@ -696,10 +701,13 @@ describe('useLoad3d', () => {
|
||||
'backgroundImageLoadingEnd',
|
||||
'modelLoadingStart',
|
||||
'modelLoadingEnd',
|
||||
'skeletonVisibilityChange',
|
||||
'exportLoadingStart',
|
||||
'exportLoadingEnd',
|
||||
'recordingStatusChange',
|
||||
'animationListChange'
|
||||
'animationListChange',
|
||||
'animationProgressChange',
|
||||
'cameraChanged'
|
||||
]
|
||||
|
||||
expectedEvents.forEach((event) => {
|
||||
@@ -40,9 +40,12 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
|
||||
const modelConfig = ref<ModelConfig>({
|
||||
upDirection: 'original',
|
||||
materialMode: 'original'
|
||||
materialMode: 'original',
|
||||
showSkeleton: false
|
||||
})
|
||||
|
||||
const hasSkeleton = ref(false)
|
||||
|
||||
const cameraConfig = ref<CameraConfig>({
|
||||
cameraType: 'perspective',
|
||||
fov: 75
|
||||
@@ -273,6 +276,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
nodeRef.value.properties['Model Config'] = newValue
|
||||
load3d.setUpDirection(newValue.upDirection)
|
||||
load3d.setMaterialMode(newValue.materialMode)
|
||||
load3d.setShowSkeleton(newValue.showSkeleton)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
@@ -503,6 +507,12 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
loading.value = false
|
||||
isSplatModel.value = load3d?.isSplatModel() ?? false
|
||||
isPlyModel.value = load3d?.isPlyModel() ?? false
|
||||
hasSkeleton.value = load3d?.hasSkeleton() ?? false
|
||||
// Reset skeleton visibility when loading new model
|
||||
modelConfig.value.showSkeleton = false
|
||||
},
|
||||
skeletonVisibilityChange: (value: boolean) => {
|
||||
modelConfig.value.showSkeleton = value
|
||||
},
|
||||
exportLoadingStart: (message: string) => {
|
||||
loadingMessage.value = message || t('load3d.exportingModel')
|
||||
@@ -584,6 +594,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
isPreview,
|
||||
isSplatModel,
|
||||
isPlyModel,
|
||||
hasSkeleton,
|
||||
hasRecording,
|
||||
recordingDuration,
|
||||
animations,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
@@ -19,10 +20,22 @@ const defaultSettingStore = {
|
||||
set: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
|
||||
const defaultRankingStore = {
|
||||
computeDefaultScore: vi.fn(() => 0),
|
||||
computePopularScore: vi.fn(() => 0),
|
||||
getUsageScore: vi.fn(() => 0),
|
||||
computeFreshness: vi.fn(() => 0.5),
|
||||
isLoaded: { value: false }
|
||||
}
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => defaultSettingStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/templateRankingStore', () => ({
|
||||
useTemplateRankingStore: vi.fn(() => defaultRankingStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => ({
|
||||
trackTemplateFilterChanged: vi.fn()
|
||||
@@ -34,6 +47,7 @@ const { useTemplateFiltering } =
|
||||
|
||||
describe('useTemplateFiltering', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
@@ -258,4 +272,108 @@ describe('useTemplateFiltering', () => {
|
||||
'beta-pro'
|
||||
])
|
||||
})
|
||||
|
||||
it('incorporates search relevance into recommended sorting', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const templates = ref<TemplateInfo[]>([
|
||||
{
|
||||
name: 'wan-video-exact',
|
||||
title: 'Wan Video Template',
|
||||
description: 'A template with Wan in title',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
date: '2024-01-01',
|
||||
usage: 10
|
||||
},
|
||||
{
|
||||
name: 'qwen-image-partial',
|
||||
title: 'Qwen Image Editor',
|
||||
description: 'A template that contains w, a, n scattered',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
date: '2024-01-01',
|
||||
usage: 1000 // Higher usage but worse search match
|
||||
},
|
||||
{
|
||||
name: 'wan-text-exact',
|
||||
title: 'Wan2.5: Text to Image',
|
||||
description: 'Another exact match for Wan',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
date: '2024-01-01',
|
||||
usage: 50
|
||||
}
|
||||
])
|
||||
|
||||
const { searchQuery, sortBy, filteredTemplates } =
|
||||
useTemplateFiltering(templates)
|
||||
|
||||
// Search for "Wan"
|
||||
searchQuery.value = 'Wan'
|
||||
sortBy.value = 'recommended'
|
||||
await nextTick()
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
// Templates with "Wan" in title should rank higher than Qwen despite lower usage
|
||||
// because search relevance is now factored into the recommended sort
|
||||
const results = filteredTemplates.value.map((t) => t.name)
|
||||
|
||||
// Verify exact matches appear (Qwen might be filtered out by threshold)
|
||||
expect(results).toContain('wan-video-exact')
|
||||
expect(results).toContain('wan-text-exact')
|
||||
|
||||
// If Qwen appears, it should be ranked lower than exact matches
|
||||
if (results.includes('qwen-image-partial')) {
|
||||
const wanIndex = results.indexOf('wan-video-exact')
|
||||
const qwenIndex = results.indexOf('qwen-image-partial')
|
||||
expect(wanIndex).toBeLessThan(qwenIndex)
|
||||
}
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('preserves Fuse search order when using default sort', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
const templates = ref<TemplateInfo[]>([
|
||||
{
|
||||
name: 'portrait-basic',
|
||||
title: 'Basic Portrait',
|
||||
description: 'A basic template',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
},
|
||||
{
|
||||
name: 'portrait-pro',
|
||||
title: 'Portrait Pro Edition',
|
||||
description: 'Advanced portrait features',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
},
|
||||
{
|
||||
name: 'landscape-view',
|
||||
title: 'Landscape Generator',
|
||||
description: 'Generate landscapes',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
}
|
||||
])
|
||||
|
||||
const { searchQuery, sortBy, filteredTemplates } =
|
||||
useTemplateFiltering(templates)
|
||||
|
||||
searchQuery.value = 'Portrait Pro'
|
||||
sortBy.value = 'default'
|
||||
await nextTick()
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await nextTick()
|
||||
|
||||
const results = filteredTemplates.value.map((t) => t.name)
|
||||
|
||||
// With default sort, Fuse's relevance ordering is preserved
|
||||
// "Portrait Pro Edition" should be first as it's the best match
|
||||
expect(results[0]).toBe('portrait-pro')
|
||||
})
|
||||
})
|
||||
@@ -6,12 +6,14 @@ import type { Ref } from 'vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { TemplateInfo } from '@/platform/workflow/templates/types/template'
|
||||
import { useTemplateRankingStore } from '@/stores/templateRankingStore'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
|
||||
export function useTemplateFiltering(
|
||||
templates: Ref<TemplateInfo[]> | TemplateInfo[]
|
||||
) {
|
||||
const settingStore = useSettingStore()
|
||||
const rankingStore = useTemplateRankingStore()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedModels = ref<string[]>(
|
||||
@@ -25,6 +27,8 @@ export function useTemplateFiltering(
|
||||
)
|
||||
const sortBy = ref<
|
||||
| 'default'
|
||||
| 'recommended'
|
||||
| 'popular'
|
||||
| 'alphabetical'
|
||||
| 'newest'
|
||||
| 'vram-low-to-high'
|
||||
@@ -78,13 +82,31 @@ export function useTemplateFiltering(
|
||||
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 50)
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
// Store Fuse search results with scores for use in sorting
|
||||
const fuseSearchResults = computed(() => {
|
||||
if (!debouncedSearchQuery.value.trim()) {
|
||||
return null
|
||||
}
|
||||
return fuse.value.search(debouncedSearchQuery.value)
|
||||
})
|
||||
|
||||
// Map of template name to search score (lower is better in Fuse, 0 = perfect match)
|
||||
const searchScoreMap = computed(() => {
|
||||
const map = new Map<string, number>()
|
||||
if (fuseSearchResults.value) {
|
||||
fuseSearchResults.value.forEach((result) => {
|
||||
// Store the score (0 = perfect match, 1 = worst match)
|
||||
map.set(result.item.name, result.score ?? 1)
|
||||
})
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const filteredBySearch = computed(() => {
|
||||
if (!fuseSearchResults.value) {
|
||||
return templatesArray.value
|
||||
}
|
||||
|
||||
const results = fuse.value.search(debouncedSearchQuery.value)
|
||||
return results.map((result) => result.item)
|
||||
return fuseSearchResults.value.map((result) => result.item)
|
||||
})
|
||||
|
||||
const filteredByModels = computed(() => {
|
||||
@@ -151,10 +173,77 @@ export function useTemplateFiltering(
|
||||
return Number.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
watch(
|
||||
filteredByRunsOn,
|
||||
(templates) => {
|
||||
rankingStore.largestUsageScore = Math.max(
|
||||
...templates.map((t) => t.usage || 0)
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Helper to get search relevance score (higher is better, 0-1 range)
|
||||
// Fuse returns scores where 0 = perfect match, 1 = worst match
|
||||
// We invert it so higher = better for combining with other scores
|
||||
const getSearchRelevance = (template: TemplateInfo): number => {
|
||||
const fuseScore = searchScoreMap.value.get(template.name)
|
||||
if (fuseScore === undefined) return 0 // Not in search results or no search
|
||||
return 1 - fuseScore // Invert: 0 (worst) -> 1 (best)
|
||||
}
|
||||
|
||||
const hasActiveSearch = computed(
|
||||
() => debouncedSearchQuery.value.trim() !== ''
|
||||
)
|
||||
|
||||
const sortedTemplates = computed(() => {
|
||||
const templates = [...filteredByRunsOn.value]
|
||||
|
||||
switch (sortBy.value) {
|
||||
case 'recommended':
|
||||
// When searching, heavily weight search relevance
|
||||
// Formula with search: searchRelevance × 0.6 + (usage × 0.5 + internal × 0.3 + freshness × 0.2) × 0.4
|
||||
// Formula without search: usage × 0.5 + internal × 0.3 + freshness × 0.2
|
||||
return templates.sort((a, b) => {
|
||||
const baseScoreA = rankingStore.computeDefaultScore(
|
||||
a.date,
|
||||
a.searchRank,
|
||||
a.usage
|
||||
)
|
||||
const baseScoreB = rankingStore.computeDefaultScore(
|
||||
b.date,
|
||||
b.searchRank,
|
||||
b.usage
|
||||
)
|
||||
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
const finalA = searchA * 0.6 + baseScoreA * 0.4
|
||||
const finalB = searchB * 0.6 + baseScoreB * 0.4
|
||||
return finalB - finalA
|
||||
}
|
||||
|
||||
return baseScoreB - baseScoreA
|
||||
})
|
||||
case 'popular':
|
||||
// When searching, include search relevance
|
||||
// Formula with search: searchRelevance × 0.5 + (usage × 0.9 + freshness × 0.1) × 0.5
|
||||
// Formula without search: usage × 0.9 + freshness × 0.1
|
||||
return templates.sort((a, b) => {
|
||||
const baseScoreA = rankingStore.computePopularScore(a.date, a.usage)
|
||||
const baseScoreB = rankingStore.computePopularScore(b.date, b.usage)
|
||||
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
const finalA = searchA * 0.5 + baseScoreA * 0.5
|
||||
const finalB = searchB * 0.5 + baseScoreB * 0.5
|
||||
return finalB - finalA
|
||||
}
|
||||
|
||||
return baseScoreB - baseScoreA
|
||||
})
|
||||
case 'alphabetical':
|
||||
return templates.sort((a, b) => {
|
||||
const nameA = a.title || a.name || ''
|
||||
@@ -173,6 +262,12 @@ export function useTemplateFiltering(
|
||||
const vramB = getVramMetric(b)
|
||||
|
||||
if (vramA === vramB) {
|
||||
// Use search relevance as tiebreaker when searching
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
if (searchA !== searchB) return searchB - searchA
|
||||
}
|
||||
const nameA = a.title || a.name || ''
|
||||
const nameB = b.title || b.name || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
@@ -184,17 +279,25 @@ export function useTemplateFiltering(
|
||||
return vramA - vramB
|
||||
})
|
||||
case 'model-size-low-to-high':
|
||||
return templates.sort((a: any, b: any) => {
|
||||
return templates.sort((a, b) => {
|
||||
const sizeA =
|
||||
typeof a.size === 'number' ? a.size : Number.POSITIVE_INFINITY
|
||||
const sizeB =
|
||||
typeof b.size === 'number' ? b.size : Number.POSITIVE_INFINITY
|
||||
if (sizeA === sizeB) return 0
|
||||
if (sizeA === sizeB) {
|
||||
// Use search relevance as tiebreaker when searching
|
||||
if (hasActiveSearch.value) {
|
||||
const searchA = getSearchRelevance(a)
|
||||
const searchB = getSearchRelevance(b)
|
||||
if (searchA !== searchB) return searchB - searchA
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return sizeA - sizeB
|
||||
})
|
||||
case 'default':
|
||||
default:
|
||||
// Keep original order (default order)
|
||||
// 'default' preserves Fuse's search order (already sorted by relevance)
|
||||
return templates
|
||||
}
|
||||
})
|
||||
@@ -206,7 +309,7 @@ export function useTemplateFiltering(
|
||||
selectedModels.value = []
|
||||
selectedUseCases.value = []
|
||||
selectedRunsOn.value = []
|
||||
sortBy.value = 'newest'
|
||||
sortBy.value = 'default'
|
||||
}
|
||||
|
||||
const removeModelFilter = (model: string) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { LGraphCanvas, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from '../litegraph/subgraph/fixtures/subgraphHelpers'
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
|
||||
const canvasEl: Partial<HTMLCanvasElement> = { addEventListener() {} }
|
||||
const canvas: Partial<LGraphCanvas> = { canvas: canvasEl as HTMLCanvasElement }
|
||||
@@ -94,7 +94,7 @@ function dynamicComboWidget(
|
||||
const newSpec = value ? options[value] : undefined
|
||||
|
||||
const removedInputs = remove(node.inputs, isInGroup)
|
||||
remove(node.widgets, isInGroup)
|
||||
for (const widget of remove(node.widgets, isInGroup)) widget.onRemove?.()
|
||||
|
||||
if (!newSpec) return
|
||||
|
||||
@@ -341,10 +341,16 @@ function applyMatchType(node: LGraphNode, inputSpec: InputSpecV2) {
|
||||
//TODO: instead apply on output add?
|
||||
//ensure outputs get updated
|
||||
const index = node.inputs.length - 1
|
||||
const input = node.inputs.at(-1)!
|
||||
requestAnimationFrame(() =>
|
||||
node.onConnectionsChange(LiteGraph.INPUT, index, false, undefined, input)
|
||||
)
|
||||
requestAnimationFrame(() => {
|
||||
const input = node.inputs.at(index)!
|
||||
node.onConnectionsChange?.(
|
||||
LiteGraph.INPUT,
|
||||
index,
|
||||
!!input.link,
|
||||
input.link ? node.graph?.links?.[input.link] : undefined,
|
||||
input
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function autogrowOrdinalToName(
|
||||
@@ -482,7 +488,8 @@ function autogrowInputDisconnected(index: number, node: AutogrowNode) {
|
||||
for (const input of toRemove) {
|
||||
const widgetName = input?.widget?.name
|
||||
if (!widgetName) continue
|
||||
remove(node.widgets, (w) => w.name === widgetName)
|
||||
for (const widget of remove(node.widgets, (w) => w.name === widgetName))
|
||||
widget.onRemove?.()
|
||||
}
|
||||
node.size[1] = node.computeSize([...node.size])[1]
|
||||
}
|
||||
|
||||
@@ -196,8 +196,7 @@ export class GroupNodeConfig {
|
||||
primitiveToWidget: {}
|
||||
nodeInputs: {}
|
||||
outputVisibility: any[]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodeDef: ComfyNodeDef
|
||||
nodeDef: (ComfyNodeDef & { [GROUP]: GroupNodeConfig }) | undefined
|
||||
// @ts-expect-error fixme ts strict error
|
||||
inputs: any[]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
@@ -231,8 +230,7 @@ export class GroupNodeConfig {
|
||||
output: [],
|
||||
output_name: [],
|
||||
output_is_list: [],
|
||||
// @ts-expect-error Unused, doesn't exist
|
||||
output_is_hidden: [],
|
||||
output_node: false, // This is a lie (to satisfy the interface)
|
||||
name: source + SEPARATOR + this.name,
|
||||
display_name: this.name,
|
||||
category: 'group nodes' + (SEPARATOR + source),
|
||||
@@ -261,6 +259,7 @@ export class GroupNodeConfig {
|
||||
}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.#convertedToProcess = null
|
||||
if (!this.nodeDef) return
|
||||
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
|
||||
useNodeDefStore().addNodeDef(this.nodeDef)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ export class CameraManager implements CameraManagerInterface {
|
||||
orthographicCamera: THREE.OrthographicCamera
|
||||
activeCamera: THREE.Camera
|
||||
|
||||
// @ts-expect-error unused variable
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private eventManager: EventManagerInterface
|
||||
|
||||
private controls: OrbitControls | null = null
|
||||
@@ -42,10 +40,9 @@ export class CameraManager implements CameraManagerInterface {
|
||||
}
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
_renderer: THREE.WebGLRenderer,
|
||||
eventManager: EventManagerInterface
|
||||
) {
|
||||
this.renderer = renderer
|
||||
this.eventManager = eventManager
|
||||
|
||||
this.perspectiveCamera = new THREE.PerspectiveCamera(
|
||||
|
||||
@@ -156,8 +156,9 @@ class Load3DConfiguration {
|
||||
|
||||
return {
|
||||
upDirection: 'original',
|
||||
materialMode: 'original'
|
||||
} as ModelConfig
|
||||
materialMode: 'original',
|
||||
showSkeleton: false
|
||||
}
|
||||
}
|
||||
|
||||
private applySceneConfig(config: SceneConfig, bgImagePath?: string) {
|
||||
|
||||
@@ -392,7 +392,8 @@ class Load3d {
|
||||
this.STATUS_MOUSE_ON_SCENE ||
|
||||
this.STATUS_MOUSE_ON_VIEWER ||
|
||||
this.isRecording() ||
|
||||
!this.INITIAL_RENDER_DONE
|
||||
!this.INITIAL_RENDER_DONE ||
|
||||
this.animationManager.isAnimationPlaying
|
||||
)
|
||||
}
|
||||
|
||||
@@ -726,6 +727,19 @@ class Load3d {
|
||||
return this.animationManager.animationClips.length > 0
|
||||
}
|
||||
|
||||
public hasSkeleton(): boolean {
|
||||
return this.modelManager.hasSkeleton()
|
||||
}
|
||||
|
||||
public setShowSkeleton(show: boolean): void {
|
||||
this.modelManager.setShowSkeleton(show)
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
public getShowSkeleton(): boolean {
|
||||
return this.modelManager.showSkeleton
|
||||
}
|
||||
|
||||
public getAnimationTime(): number {
|
||||
return this.animationManager.getAnimationTime()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
import { PLYLoader } from 'three/examples/jsm/loaders/PLYLoader'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { MtlObjBridge, OBJLoader2Parallel } from 'wwobjloader2'
|
||||
import OBJLoader2WorkerUrl from 'wwobjloader2/worker?url'
|
||||
// Use pre-bundled worker module (has all dependencies included)
|
||||
// The unbundled 'wwobjloader2/worker' has ES imports that fail in production builds
|
||||
import OBJLoader2WorkerUrl from 'wwobjloader2/bundle/worker/module?url'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -166,6 +168,10 @@ export class LoaderManager implements LoaderManagerInterface {
|
||||
fbxModel.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
|
||||
if (child instanceof THREE.SkinnedMesh) {
|
||||
child.frustumCulled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
@@ -212,6 +218,10 @@ export class LoaderManager implements LoaderManagerInterface {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry.computeVertexNormals()
|
||||
this.modelManager.originalMaterials.set(child, child.material)
|
||||
|
||||
if (child instanceof THREE.SkinnedMesh) {
|
||||
child.frustumCulled = false
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
|
||||
@@ -27,13 +27,11 @@ export class SceneManager implements SceneManagerInterface {
|
||||
private renderer: THREE.WebGLRenderer
|
||||
|
||||
private getActiveCamera: () => THREE.Camera
|
||||
// @ts-expect-error unused variable
|
||||
private getControls: () => OrbitControls
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
getActiveCamera: () => THREE.Camera,
|
||||
getControls: () => OrbitControls,
|
||||
_getControls: () => OrbitControls,
|
||||
eventManager: EventManagerInterface
|
||||
) {
|
||||
this.renderer = renderer
|
||||
@@ -41,7 +39,6 @@ export class SceneManager implements SceneManagerInterface {
|
||||
this.scene = new THREE.Scene()
|
||||
|
||||
this.getActiveCamera = getActiveCamera
|
||||
this.getControls = getControls
|
||||
|
||||
this.gridHelper = new THREE.GridHelper(20, 20)
|
||||
this.gridHelper.position.set(0, 0, 0)
|
||||
|
||||
@@ -30,6 +30,8 @@ export class SceneModelManager implements ModelManagerInterface {
|
||||
originalURL: string | null = null
|
||||
appliedTexture: THREE.Texture | null = null
|
||||
textureLoader: THREE.TextureLoader
|
||||
skeletonHelper: THREE.SkeletonHelper | null = null
|
||||
showSkeleton: boolean = false
|
||||
|
||||
private scene: THREE.Scene
|
||||
private renderer: THREE.WebGLRenderer
|
||||
@@ -414,9 +416,69 @@ export class SceneModelManager implements ModelManagerInterface {
|
||||
this.appliedTexture = null
|
||||
}
|
||||
|
||||
if (this.skeletonHelper) {
|
||||
this.scene.remove(this.skeletonHelper)
|
||||
this.skeletonHelper.dispose()
|
||||
this.skeletonHelper = null
|
||||
}
|
||||
this.showSkeleton = false
|
||||
|
||||
this.originalMaterials = new WeakMap()
|
||||
}
|
||||
|
||||
hasSkeleton(): boolean {
|
||||
if (!this.currentModel) return false
|
||||
let found = false
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.SkinnedMesh && child.skeleton) {
|
||||
found = true
|
||||
}
|
||||
})
|
||||
return found
|
||||
}
|
||||
|
||||
setShowSkeleton(show: boolean): void {
|
||||
this.showSkeleton = show
|
||||
|
||||
if (show) {
|
||||
if (!this.skeletonHelper && this.currentModel) {
|
||||
let rootBone: THREE.Bone | null = null
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.Bone && !rootBone) {
|
||||
if (!(child.parent instanceof THREE.Bone)) {
|
||||
rootBone = child
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (rootBone) {
|
||||
this.skeletonHelper = new THREE.SkeletonHelper(rootBone)
|
||||
this.scene.add(this.skeletonHelper)
|
||||
} else {
|
||||
let skinnedMesh: THREE.SkinnedMesh | null = null
|
||||
this.currentModel.traverse((child) => {
|
||||
if (child instanceof THREE.SkinnedMesh && !skinnedMesh) {
|
||||
skinnedMesh = child
|
||||
}
|
||||
})
|
||||
|
||||
if (skinnedMesh) {
|
||||
this.skeletonHelper = new THREE.SkeletonHelper(skinnedMesh)
|
||||
this.scene.add(this.skeletonHelper)
|
||||
}
|
||||
}
|
||||
} else if (this.skeletonHelper) {
|
||||
this.skeletonHelper.visible = true
|
||||
}
|
||||
} else {
|
||||
if (this.skeletonHelper) {
|
||||
this.skeletonHelper.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
this.eventManager.emitEvent('skeletonVisibilityChange', show)
|
||||
}
|
||||
|
||||
addModelToScene(model: THREE.Object3D): void {
|
||||
this.currentModel = model
|
||||
model.name = 'MainModel'
|
||||
|
||||
@@ -14,16 +14,13 @@ export class ViewHelperManager implements ViewHelperManagerInterface {
|
||||
private getActiveCamera: () => THREE.Camera
|
||||
private getControls: () => OrbitControls
|
||||
private eventManager: EventManagerInterface
|
||||
// @ts-expect-error unused variable
|
||||
private renderer: THREE.WebGLRenderer
|
||||
|
||||
constructor(
|
||||
renderer: THREE.WebGLRenderer,
|
||||
_renderer: THREE.WebGLRenderer,
|
||||
getActiveCamera: () => THREE.Camera,
|
||||
getControls: () => OrbitControls,
|
||||
eventManager: EventManagerInterface
|
||||
) {
|
||||
this.renderer = renderer
|
||||
this.getActiveCamera = getActiveCamera
|
||||
this.getControls = getControls
|
||||
this.eventManager = eventManager
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface SceneConfig {
|
||||
export interface ModelConfig {
|
||||
upDirection: UpDirection
|
||||
materialMode: MaterialMode
|
||||
showSkeleton: boolean
|
||||
}
|
||||
|
||||
export interface CameraConfig {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { dirtyTest } from './fixtures/testExtensions'
|
||||
import { dirtyTest } from './__fixtures__/testExtensions'
|
||||
|
||||
describe.skip('LGraph configure()', () => {
|
||||
dirtyTest(
|
||||
@@ -3,7 +3,7 @@ import { describe } from 'vitest'
|
||||
|
||||
import { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { dirtyTest } from './fixtures/testExtensions'
|
||||
import { dirtyTest } from './__fixtures__/testExtensions'
|
||||
|
||||
describe.skip('LGraph (constructor only)', () => {
|
||||
dirtyTest(
|
||||
@@ -3,7 +3,7 @@ import { describe } from 'vitest'
|
||||
import { LGraph, LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LGraph Serialisation', () => {
|
||||
test('can (de)serialise node / group titles', ({ expect, minimalGraph }) => {
|
||||
@@ -1,10 +1,43 @@
|
||||
import { describe } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
function swapNodes(nodes: LGraphNode[]) {
|
||||
const firstNode = nodes[0]
|
||||
const lastNode = nodes[nodes.length - 1]
|
||||
nodes[0] = lastNode
|
||||
nodes[nodes.length - 1] = firstNode
|
||||
return nodes
|
||||
}
|
||||
|
||||
function createGraph(...nodes: LGraphNode[]) {
|
||||
const graph = new LGraph()
|
||||
nodes.forEach((node) => graph.add(node))
|
||||
return graph
|
||||
}
|
||||
|
||||
class DummyNode extends LGraphNode {
|
||||
constructor() {
|
||||
super('dummy')
|
||||
}
|
||||
}
|
||||
|
||||
describe('LGraph', () => {
|
||||
it('should serialize deterministic node order', async () => {
|
||||
LiteGraph.registerNodeType('dummy', DummyNode)
|
||||
const node1 = new DummyNode()
|
||||
const node2 = new DummyNode()
|
||||
const graph = createGraph(node1, node2)
|
||||
|
||||
const result1 = graph.serialize({ sortNodes: true })
|
||||
expect(result1.nodes).not.toHaveLength(0)
|
||||
graph._nodes = swapNodes(graph.nodes)
|
||||
const result2 = graph.serialize({ sortNodes: true })
|
||||
|
||||
expect(result1).toEqual(result2)
|
||||
})
|
||||
test('can be instantiated', ({ expect }) => {
|
||||
// @ts-expect-error Intentional - extra holds any / all consumer data that should be serialised
|
||||
const graph = new LGraph({ extra: 'TestGraph' })
|
||||
@@ -1,21 +1,18 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
|
||||
import { Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphButton, Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('LGraphButton', () => {
|
||||
describe('Constructor', () => {
|
||||
it('should create a button with default options', () => {
|
||||
// @ts-expect-error TODO: Fix after merge - LGraphButton constructor type issues
|
||||
const button = new LGraphButton({})
|
||||
const button = new LGraphButton({ text: '' })
|
||||
expect(button).toBeInstanceOf(LGraphButton)
|
||||
expect(button.name).toBeUndefined()
|
||||
expect(button._last_area).toBeInstanceOf(Rectangle)
|
||||
})
|
||||
|
||||
it('should create a button with custom name', () => {
|
||||
// @ts-expect-error TODO: Fix after merge - LGraphButton constructor type issues
|
||||
const button = new LGraphButton({ name: 'test_button' })
|
||||
const button = new LGraphButton({ text: '', name: 'test_button' })
|
||||
expect(button.name).toBe('test_button')
|
||||
})
|
||||
|
||||
@@ -159,9 +156,8 @@ describe('LGraphButton', () => {
|
||||
const button = new LGraphButton({
|
||||
text: '→',
|
||||
fontSize: 20,
|
||||
// @ts-expect-error TODO: Fix after merge - color property not defined in type
|
||||
color: '#FFFFFF',
|
||||
backgroundColor: '#333333',
|
||||
fgColor: '#FFFFFF',
|
||||
bgColor: '#333333',
|
||||
xOffset: -10,
|
||||
yOffset: 5
|
||||
})
|
||||
@@ -1,7 +1,11 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('LGraphCanvas Title Button Rendering', () => {
|
||||
let canvas: LGraphCanvas
|
||||
@@ -43,8 +47,8 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
|
||||
canvasElement.getContext = vi.fn().mockReturnValue(ctx)
|
||||
|
||||
// @ts-expect-error TODO: Fix after merge - LGraphCanvas constructor type issues
|
||||
canvas = new LGraphCanvas(canvasElement, null, {
|
||||
const graph = new LGraph()
|
||||
canvas = new LGraphCanvas(canvasElement, graph, {
|
||||
skip_render: true,
|
||||
skip_events: true
|
||||
})
|
||||
@@ -53,18 +57,9 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
node.pos = [100, 200]
|
||||
node.size = [200, 100]
|
||||
|
||||
// Mock required methods
|
||||
node.drawTitleBarBackground = vi.fn()
|
||||
// @ts-expect-error Property 'drawTitleBarText' does not exist on type 'LGraphNode'
|
||||
node.drawTitleBarText = vi.fn()
|
||||
node.drawBadges = vi.fn()
|
||||
// @ts-expect-error TODO: Fix after merge - drawToggles not defined in type
|
||||
node.drawToggles = vi.fn()
|
||||
// @ts-expect-error TODO: Fix after merge - drawNodeShape not defined in type
|
||||
node.drawNodeShape = vi.fn()
|
||||
node.drawSlots = vi.fn()
|
||||
// @ts-expect-error TODO: Fix after merge - drawContent not defined in type
|
||||
node.drawContent = vi.fn()
|
||||
node.drawWidgets = vi.fn()
|
||||
node.drawCollapsedSlots = vi.fn()
|
||||
node.drawTitleBox = vi.fn()
|
||||
@@ -72,24 +67,31 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
node.drawProgressBar = vi.fn()
|
||||
node._setConcreteSlots = vi.fn()
|
||||
node.arrange = vi.fn()
|
||||
// @ts-expect-error TODO: Fix after merge - isSelectable not defined in type
|
||||
node.isSelectable = vi.fn().mockReturnValue(true)
|
||||
|
||||
const nodeWithMocks = node as LGraphNode & {
|
||||
drawTitleBarText: ReturnType<typeof vi.fn>
|
||||
drawToggles: ReturnType<typeof vi.fn>
|
||||
drawNodeShape: ReturnType<typeof vi.fn>
|
||||
drawContent: ReturnType<typeof vi.fn>
|
||||
isSelectable: ReturnType<typeof vi.fn>
|
||||
}
|
||||
nodeWithMocks.drawTitleBarText = vi.fn()
|
||||
nodeWithMocks.drawToggles = vi.fn()
|
||||
nodeWithMocks.drawNodeShape = vi.fn()
|
||||
nodeWithMocks.drawContent = vi.fn()
|
||||
nodeWithMocks.isSelectable = vi.fn().mockReturnValue(true)
|
||||
})
|
||||
|
||||
describe('drawNode title button rendering', () => {
|
||||
it('should render visible title buttons', () => {
|
||||
const button1 = node.addTitleButton({
|
||||
name: 'button1',
|
||||
text: 'A',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'A'
|
||||
})
|
||||
|
||||
const button2 = node.addTitleButton({
|
||||
name: 'button2',
|
||||
text: 'B',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'B'
|
||||
})
|
||||
|
||||
// Mock button methods
|
||||
@@ -124,9 +126,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
it('should skip invisible title buttons', () => {
|
||||
const visibleButton = node.addTitleButton({
|
||||
name: 'visible',
|
||||
text: 'V',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'V'
|
||||
})
|
||||
|
||||
const invisibleButton = node.addTitleButton({
|
||||
@@ -168,9 +168,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const button = node.addTitleButton({
|
||||
name: `button${i}`,
|
||||
text: String(i),
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: String(i)
|
||||
})
|
||||
button.getWidth = vi.fn().mockReturnValue(15) // All same width for simplicity
|
||||
const spy = vi.spyOn(button, 'draw')
|
||||
@@ -193,18 +191,12 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
it('should render buttons in low quality mode', () => {
|
||||
const button = node.addTitleButton({
|
||||
name: 'test',
|
||||
text: 'T',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'T'
|
||||
})
|
||||
|
||||
button.getWidth = vi.fn().mockReturnValue(20)
|
||||
const drawSpy = vi.spyOn(button, 'draw')
|
||||
|
||||
// Set low quality rendering
|
||||
// @ts-expect-error TODO: Fix after merge - lowQualityRenderingRequired not defined in type
|
||||
canvas.lowQualityRenderingRequired = true
|
||||
|
||||
canvas.drawNode(node, ctx)
|
||||
|
||||
// Buttons should still be rendered in low quality mode
|
||||
@@ -216,16 +208,12 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
it('should handle buttons with different widths', () => {
|
||||
const smallButton = node.addTitleButton({
|
||||
name: 'small',
|
||||
text: 'S',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'S'
|
||||
})
|
||||
|
||||
const largeButton = node.addTitleButton({
|
||||
name: 'large',
|
||||
text: 'LARGE',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'LARGE'
|
||||
})
|
||||
|
||||
smallButton.getWidth = vi.fn().mockReturnValue(15)
|
||||
@@ -253,9 +241,7 @@ describe('LGraphCanvas Title Button Rendering', () => {
|
||||
|
||||
const button = node.addTitleButton({
|
||||
name: 'test',
|
||||
text: 'X',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not in LGraphButtonOptions
|
||||
visible: true
|
||||
text: 'X'
|
||||
})
|
||||
|
||||
button.getWidth = vi.fn().mockReturnValue(20)
|
||||
@@ -969,10 +969,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onGroupAdd(
|
||||
// @ts-expect-error - unused parameter
|
||||
info: unknown,
|
||||
// @ts-expect-error - unused parameter
|
||||
entry: unknown,
|
||||
_info: unknown,
|
||||
_entry: unknown,
|
||||
mouse_event: MouseEvent
|
||||
): void {
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
@@ -1020,10 +1018,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onNodeAlign(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
event: MouseEvent,
|
||||
prev_menu: ContextMenu<string>,
|
||||
node: LGraphNode
|
||||
@@ -1046,10 +1042,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onGroupAlign(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
event: MouseEvent,
|
||||
prev_menu: ContextMenu<string>
|
||||
): void {
|
||||
@@ -1070,10 +1064,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static createDistributeMenu(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
event: MouseEvent,
|
||||
prev_menu: ContextMenu<string>
|
||||
): void {
|
||||
@@ -1095,16 +1087,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onMenuAdd(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: unknown,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: unknown,
|
||||
_value: unknown,
|
||||
_options: unknown,
|
||||
e: MouseEvent,
|
||||
prev_menu?: ContextMenu<string>,
|
||||
callback?: (node: LGraphNode | null) => void
|
||||
): boolean | undefined {
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
const ref_window = canvas.getCanvasWindow()
|
||||
const { graph } = canvas
|
||||
if (!graph) return
|
||||
|
||||
@@ -1155,14 +1144,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
value: category_path,
|
||||
content: name,
|
||||
has_submenu: true,
|
||||
callback: function (
|
||||
value,
|
||||
// @ts-expect-error - unused parameter
|
||||
event,
|
||||
// @ts-expect-error - unused parameter
|
||||
mouseEvent,
|
||||
contextMenu
|
||||
) {
|
||||
callback: function (value, _event, _mouseEvent, contextMenu) {
|
||||
inner_onMenuAdded(value.value, contextMenu)
|
||||
}
|
||||
})
|
||||
@@ -1181,14 +1163,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
value: node.type,
|
||||
content: node.title,
|
||||
has_submenu: false,
|
||||
callback: function (
|
||||
value,
|
||||
// @ts-expect-error - unused parameter
|
||||
event,
|
||||
// @ts-expect-error - unused parameter
|
||||
mouseEvent,
|
||||
contextMenu
|
||||
) {
|
||||
callback: function (value, _event, _mouseEvent, contextMenu) {
|
||||
if (!canvas.graph) throw new NullGraphError()
|
||||
|
||||
const first_event = contextMenu.getFirstEvent()
|
||||
@@ -1213,12 +1188,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
entries.push(entry)
|
||||
}
|
||||
|
||||
new LiteGraph.ContextMenu(
|
||||
entries,
|
||||
{ event: e, parentMenu: prev_menu },
|
||||
// @ts-expect-error - extra parameter
|
||||
ref_window
|
||||
)
|
||||
new LiteGraph.ContextMenu(entries, { event: e, parentMenu: prev_menu })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1227,8 +1197,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
/** @param _options Parameter is never used */
|
||||
static showMenuNodeOptionalOutputs(
|
||||
// @ts-expect-error - unused parameter
|
||||
v: unknown,
|
||||
_v: unknown,
|
||||
/** Unused - immediately overwritten */
|
||||
_options: INodeOutputSlot[],
|
||||
e: MouseEvent,
|
||||
@@ -1312,8 +1281,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
/** @param value Parameter is never used */
|
||||
static onShowMenuNodeProperties(
|
||||
value: NodeProperty | undefined,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: unknown,
|
||||
_options: unknown,
|
||||
e: MouseEvent,
|
||||
prev_menu: ContextMenu<string>,
|
||||
node: LGraphNode
|
||||
@@ -1321,7 +1289,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (!node || !node.properties) return
|
||||
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
const ref_window = canvas.getCanvasWindow()
|
||||
|
||||
const entries: IContextMenuValue<string>[] = []
|
||||
for (const i in node.properties) {
|
||||
@@ -1344,23 +1311,20 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
return
|
||||
}
|
||||
|
||||
new LiteGraph.ContextMenu<string>(
|
||||
entries,
|
||||
{
|
||||
event: e,
|
||||
callback: inner_clicked,
|
||||
parentMenu: prev_menu,
|
||||
allow_html: true,
|
||||
node
|
||||
},
|
||||
// @ts-expect-error Unused
|
||||
ref_window
|
||||
)
|
||||
new LiteGraph.ContextMenu<string>(entries, {
|
||||
event: e,
|
||||
callback: inner_clicked,
|
||||
parentMenu: prev_menu,
|
||||
node
|
||||
})
|
||||
|
||||
function inner_clicked(this: ContextMenuDivElement, v: { value: any }) {
|
||||
if (!node) return
|
||||
function inner_clicked(
|
||||
this: ContextMenu<string>,
|
||||
v?: string | IContextMenuValue<string>
|
||||
) {
|
||||
if (!node || typeof v === 'string' || !v?.value) return
|
||||
|
||||
const rect = this.getBoundingClientRect()
|
||||
const rect = this.root.getBoundingClientRect()
|
||||
canvas.showEditPropertyValue(node, v.value, {
|
||||
position: [rect.left, rect.top]
|
||||
})
|
||||
@@ -1377,14 +1341,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onMenuResizeNode(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
// @ts-expect-error - unused parameter
|
||||
e: MouseEvent,
|
||||
// @ts-expect-error - unused parameter
|
||||
menu: ContextMenu,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
_e: MouseEvent,
|
||||
_menu: ContextMenu,
|
||||
node: LGraphNode
|
||||
): void {
|
||||
if (!node) return
|
||||
@@ -1411,11 +1371,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// TODO refactor :: this is used fot title but not for properties!
|
||||
static onShowPropertyEditor(
|
||||
item: { property: keyof LGraphNode; type: string },
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions<string>,
|
||||
_options: IContextMenuOptions<string>,
|
||||
e: MouseEvent,
|
||||
// @ts-expect-error - unused parameter
|
||||
menu: ContextMenu<string>,
|
||||
_menu: ContextMenu<string>,
|
||||
node: LGraphNode
|
||||
): void {
|
||||
const property = item.property || 'title'
|
||||
@@ -1485,11 +1443,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
input.focus()
|
||||
|
||||
let dialogCloseTimer: number
|
||||
let dialogCloseTimer: ReturnType<typeof setTimeout> | undefined
|
||||
dialog.addEventListener('mouseleave', function () {
|
||||
if (LiteGraph.dialog_close_on_mouse_leave) {
|
||||
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) {
|
||||
// @ts-expect-error - setTimeout type
|
||||
dialogCloseTimer = setTimeout(
|
||||
dialog.close,
|
||||
LiteGraph.dialog_close_on_mouse_leave_delay
|
||||
@@ -1544,14 +1501,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onMenuNodeCollapse(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
// @ts-expect-error - unused parameter
|
||||
e: MouseEvent,
|
||||
// @ts-expect-error - unused parameter
|
||||
menu: ContextMenu,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
_e: MouseEvent,
|
||||
_menu: ContextMenu,
|
||||
node: LGraphNode
|
||||
): void {
|
||||
if (!node.graph) throw new NullGraphError()
|
||||
@@ -1578,14 +1531,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onMenuToggleAdvanced(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
// @ts-expect-error - unused parameter
|
||||
e: MouseEvent,
|
||||
// @ts-expect-error - unused parameter
|
||||
menu: ContextMenu,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
_e: MouseEvent,
|
||||
_menu: ContextMenu,
|
||||
node: LGraphNode
|
||||
): void {
|
||||
if (!node.graph) throw new NullGraphError()
|
||||
@@ -1610,10 +1559,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onMenuNodeMode(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
_value: IContextMenuValue,
|
||||
_options: IContextMenuOptions,
|
||||
e: MouseEvent,
|
||||
menu: ContextMenu,
|
||||
node: LGraphNode
|
||||
@@ -1657,8 +1604,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
/** @param value Parameter is never used */
|
||||
static onMenuNodeColors(
|
||||
value: IContextMenuValue<string | null>,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions,
|
||||
_options: IContextMenuOptions,
|
||||
e: MouseEvent,
|
||||
menu: ContextMenu<string | null>,
|
||||
node: LGraphNode
|
||||
@@ -1719,10 +1665,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
static onMenuNodeShapes(
|
||||
// @ts-expect-error - unused parameter
|
||||
value: IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]>,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: IContextMenuOptions<(typeof LiteGraph.VALID_SHAPES)[number]>,
|
||||
_value: IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]>,
|
||||
_options: IContextMenuOptions<(typeof LiteGraph.VALID_SHAPES)[number]>,
|
||||
e: MouseEvent,
|
||||
menu?: ContextMenu<(typeof LiteGraph.VALID_SHAPES)[number]>,
|
||||
node?: LGraphNode
|
||||
@@ -3596,13 +3540,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.node_over?.onMouseUp?.(
|
||||
e,
|
||||
[x - this.node_over.pos[0], y - this.node_over.pos[1]],
|
||||
// @ts-expect-error - extra parameter
|
||||
this
|
||||
)
|
||||
this.node_capturing_input?.onMouseUp?.(e, [
|
||||
x - this.node_capturing_input.pos[0],
|
||||
y - this.node_capturing_input.pos[1]
|
||||
])
|
||||
this.node_capturing_input?.onMouseUp?.(
|
||||
e,
|
||||
[
|
||||
x - this.node_capturing_input.pos[0],
|
||||
y - this.node_capturing_input.pos[1]
|
||||
],
|
||||
this
|
||||
)
|
||||
}
|
||||
} else if (e.button === 1) {
|
||||
// middle button
|
||||
@@ -4599,9 +4546,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
/**
|
||||
* converts a coordinate from graph coordinates to canvas2D coordinates
|
||||
*/
|
||||
convertOffsetToCanvas(pos: Point, out: Point): Point {
|
||||
// @ts-expect-error Unused param
|
||||
return this.ds.convertOffsetToCanvas(pos, out)
|
||||
convertOffsetToCanvas(pos: Point, _out?: Point): Point {
|
||||
return this.ds.convertOffsetToCanvas(pos)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6144,11 +6090,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
/**
|
||||
* draws every group area in the background
|
||||
*/
|
||||
drawGroups(
|
||||
// @ts-expect-error - unused parameter
|
||||
canvas: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D
|
||||
): void {
|
||||
drawGroups(_canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D): void {
|
||||
if (!this.graph) return
|
||||
|
||||
const groups = this.graph._groups
|
||||
@@ -6242,8 +6184,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
function inner_clicked(
|
||||
this: LGraphCanvas,
|
||||
v: string,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: unknown,
|
||||
_options: unknown,
|
||||
e: MouseEvent
|
||||
) {
|
||||
if (!graph) throw new NullGraphError()
|
||||
@@ -6762,13 +6703,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
if (this.ds.scale > 1) dialog.style.transform = `scale(${this.ds.scale})`
|
||||
|
||||
let dialogCloseTimer: number
|
||||
let dialogCloseTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let prevent_timeout = 0
|
||||
LiteGraph.pointerListenerAdd(dialog, 'leave', function () {
|
||||
if (prevent_timeout) return
|
||||
if (LiteGraph.dialog_close_on_mouse_leave) {
|
||||
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) {
|
||||
// @ts-expect-error - setTimeout type
|
||||
dialogCloseTimer = setTimeout(
|
||||
dialog.close,
|
||||
LiteGraph.dialog_close_on_mouse_leave_delay
|
||||
@@ -6957,7 +6897,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (options.hide_on_mouse_leave) {
|
||||
// FIXME: Remove "any" kludge
|
||||
let prevent_timeout: any = false
|
||||
let timeout_close: number | null = null
|
||||
let timeout_close: ReturnType<typeof setTimeout> | null = null
|
||||
LiteGraph.pointerListenerAdd(dialog, 'enter', function () {
|
||||
if (timeout_close) {
|
||||
clearTimeout(timeout_close)
|
||||
@@ -6969,7 +6909,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
const hideDelay = options.hide_on_mouse_leave
|
||||
const delay = typeof hideDelay === 'number' ? hideDelay : 500
|
||||
// @ts-expect-error - setTimeout type
|
||||
timeout_close = setTimeout(dialog.close, delay)
|
||||
})
|
||||
// if filtering, check focus changed to comboboxes and prevent closing
|
||||
@@ -7005,7 +6944,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
that.search_box = dialog
|
||||
|
||||
let first: string | null = null
|
||||
let timeout: number | null = null
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
let selected: ChildNode | null = null
|
||||
|
||||
const maybeInput = dialog.querySelector('input')
|
||||
@@ -7039,7 +6978,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (timeout) {
|
||||
clearInterval(timeout)
|
||||
}
|
||||
// @ts-expect-error - setTimeout type
|
||||
timeout = setTimeout(refreshHelper, 10)
|
||||
return
|
||||
}
|
||||
@@ -7314,9 +7252,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
options.show_general_after_typefiltered &&
|
||||
(sIn.value || sOut.value)
|
||||
) {
|
||||
// FIXME: Undeclared variable again
|
||||
// @ts-expect-error Variable declared without type annotation
|
||||
filtered_extra = []
|
||||
const filtered_extra: string[] = []
|
||||
for (const i in LiteGraph.registered_node_types) {
|
||||
if (
|
||||
inner_test_filter(i, {
|
||||
@@ -7324,11 +7260,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
outTypeOverride: sOut && sOut.value ? '*' : false
|
||||
})
|
||||
) {
|
||||
// @ts-expect-error Variable declared without type annotation
|
||||
filtered_extra.push(i)
|
||||
}
|
||||
}
|
||||
// @ts-expect-error Variable declared without type annotation
|
||||
for (const extraItem of filtered_extra) {
|
||||
addResult(extraItem, 'generic_type')
|
||||
if (
|
||||
@@ -7345,14 +7279,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
helper.childNodes.length == 0 &&
|
||||
options.show_general_if_none_on_typefilter
|
||||
) {
|
||||
// @ts-expect-error Variable declared without type annotation
|
||||
filtered_extra = []
|
||||
const filtered_extra: string[] = []
|
||||
for (const i in LiteGraph.registered_node_types) {
|
||||
if (inner_test_filter(i, { skipFilter: true }))
|
||||
// @ts-expect-error Variable declared without type annotation
|
||||
filtered_extra.push(i)
|
||||
}
|
||||
// @ts-expect-error Variable declared without type annotation
|
||||
for (const extraItem of filtered_extra) {
|
||||
addResult(extraItem, 'not_in_filter')
|
||||
if (
|
||||
@@ -7647,13 +7578,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
}
|
||||
|
||||
let dialogCloseTimer: number
|
||||
let dialogCloseTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let prevent_timeout = 0
|
||||
dialog.addEventListener('mouseleave', function () {
|
||||
if (prevent_timeout) return
|
||||
|
||||
if (!dialog.is_modified && LiteGraph.dialog_close_on_mouse_leave) {
|
||||
// @ts-expect-error - setTimeout type
|
||||
dialogCloseTimer = setTimeout(
|
||||
dialog.close,
|
||||
LiteGraph.dialog_close_on_mouse_leave_delay
|
||||
@@ -7687,7 +7617,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
createPanel(title: string, options: ICreatePanelOptions) {
|
||||
options = options || {}
|
||||
|
||||
const ref_window = options.window || window
|
||||
// TODO: any kludge
|
||||
const root: any = document.createElement('div')
|
||||
root.className = 'litegraph dialog'
|
||||
@@ -7865,16 +7794,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
innerChange(propname, v)
|
||||
return false
|
||||
}
|
||||
new LiteGraph.ContextMenu(
|
||||
values,
|
||||
{
|
||||
event,
|
||||
className: 'dark',
|
||||
callback: inner_clicked
|
||||
},
|
||||
// @ts-expect-error ref_window parameter unused in ContextMenu constructor
|
||||
ref_window
|
||||
)
|
||||
new LiteGraph.ContextMenu(values, {
|
||||
event,
|
||||
className: 'dark',
|
||||
// @ts-expect-error fixme ts strict error - callback signature mismatch
|
||||
callback: inner_clicked
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8194,14 +8119,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
{
|
||||
content: 'Properties Panel',
|
||||
callback: function (
|
||||
// @ts-expect-error - unused parameter
|
||||
item: any,
|
||||
// @ts-expect-error - unused parameter
|
||||
options: any,
|
||||
// @ts-expect-error - unused parameter
|
||||
e: any,
|
||||
// @ts-expect-error - unused parameter
|
||||
menu: any,
|
||||
_item: any,
|
||||
_options: any,
|
||||
_e: any,
|
||||
_menu: any,
|
||||
node: LGraphNode
|
||||
) {
|
||||
LGraphCanvas.active_canvas.showShowNodePanel(node)
|
||||
@@ -8312,9 +8233,6 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
node: LGraphNode | undefined,
|
||||
event: CanvasPointerEvent
|
||||
): void {
|
||||
const canvas = LGraphCanvas.active_canvas
|
||||
const ref_window = canvas.getCanvasWindow()
|
||||
|
||||
// TODO: Remove type kludge
|
||||
let menu_info: (IContextMenuValue | string | null)[]
|
||||
const options: IContextMenuOptions = {
|
||||
@@ -8428,8 +8346,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// show menu
|
||||
if (!menu_info) return
|
||||
|
||||
// @ts-expect-error Remove param ref_window - unused
|
||||
new LiteGraph.ContextMenu(menu_info, options, ref_window)
|
||||
new LiteGraph.ContextMenu(menu_info, options)
|
||||
|
||||
const createDialog = (options: IDialogOptions) =>
|
||||
this.createDialog(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from 'vitest'
|
||||
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LGraphGroup', () => {
|
||||
test('serializes to the existing format', () => {
|
||||
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect } from 'vitest'
|
||||
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LGraphNode resize functionality', () => {
|
||||
let node: LGraphNode
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
NodeOutputSlot
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
function getMockISerialisedNode(
|
||||
data: Partial<ISerialisedNode>
|
||||
@@ -38,7 +38,7 @@ describe('LGraphNode', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
origLiteGraph = Object.assign({}, LiteGraph)
|
||||
// @ts-expect-error TODO: Fix after merge - Classes property not in type
|
||||
// @ts-expect-error Intended: Force remove an otherwise readonly non-optional property
|
||||
delete origLiteGraph.Classes
|
||||
|
||||
Object.assign(LiteGraph, {
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphButton, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('LGraphNode Title Buttons', () => {
|
||||
describe('addTitleButton', () => {
|
||||
@@ -36,11 +35,10 @@ describe('LGraphNode Title Buttons', () => {
|
||||
expect(node.title_buttons[2]).toBe(button3)
|
||||
})
|
||||
|
||||
it('should create buttons with default options', () => {
|
||||
it('should create buttons with minimal options', () => {
|
||||
const node = new LGraphNode('Test Node')
|
||||
|
||||
// @ts-expect-error TODO: Fix after merge - addTitleButton type issues
|
||||
const button = node.addTitleButton({})
|
||||
const button = node.addTitleButton({ text: '' })
|
||||
|
||||
expect(button).toBeInstanceOf(LGraphButton)
|
||||
expect(button.name).toBeUndefined()
|
||||
@@ -56,9 +54,7 @@ describe('LGraphNode Title Buttons', () => {
|
||||
|
||||
const button = node.addTitleButton({
|
||||
name: 'close_button',
|
||||
text: 'X',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not defined in type
|
||||
visible: true
|
||||
text: 'X'
|
||||
})
|
||||
|
||||
// Mock button methods
|
||||
@@ -113,9 +109,7 @@ describe('LGraphNode Title Buttons', () => {
|
||||
|
||||
const button = node.addTitleButton({
|
||||
name: 'test_button',
|
||||
text: 'T',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not defined in type
|
||||
visible: true
|
||||
text: 'T'
|
||||
})
|
||||
|
||||
button.getWidth = vi.fn().mockReturnValue(20)
|
||||
@@ -165,16 +159,12 @@ describe('LGraphNode Title Buttons', () => {
|
||||
|
||||
const button1 = node.addTitleButton({
|
||||
name: 'button1',
|
||||
text: 'A',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not defined in type
|
||||
visible: true
|
||||
text: 'A'
|
||||
})
|
||||
|
||||
const button2 = node.addTitleButton({
|
||||
name: 'button2',
|
||||
text: 'B',
|
||||
// @ts-expect-error TODO: Fix after merge - visible property not defined in type
|
||||
visible: true
|
||||
text: 'B'
|
||||
})
|
||||
|
||||
// Mock button methods
|
||||
@@ -298,8 +288,7 @@ describe('LGraphNode Title Buttons', () => {
|
||||
describe('onTitleButtonClick', () => {
|
||||
it('should dispatch litegraph:node-title-button-clicked event', () => {
|
||||
const node = new LGraphNode('Test Node')
|
||||
// @ts-expect-error TODO: Fix after merge - LGraphButton constructor type issues
|
||||
const button = new LGraphButton({ name: 'test_button' })
|
||||
const button = new LGraphButton({ name: 'test_button', text: 'X' })
|
||||
|
||||
const canvas = {
|
||||
dispatch: vi.fn()
|
||||
@@ -679,7 +679,12 @@ export class LGraphNode
|
||||
this: LGraphNode,
|
||||
entries: (IContextMenuValue<INodeSlotContextItem> | null)[]
|
||||
): (IContextMenuValue<INodeSlotContextItem> | null)[]
|
||||
onMouseUp?(this: LGraphNode, e: CanvasPointerEvent, pos: Point): void
|
||||
onMouseUp?(
|
||||
this: LGraphNode,
|
||||
e: CanvasPointerEvent,
|
||||
pos: Point,
|
||||
canvas: LGraphCanvas
|
||||
): void
|
||||
onMouseEnter?(this: LGraphNode, e: CanvasPointerEvent): void
|
||||
/** Blocks drag if return value is truthy. @param pos Offset from {@link LGraphNode.pos}. */
|
||||
onMouseDown?(
|
||||
@@ -2769,8 +2774,7 @@ export class LGraphNode
|
||||
!LiteGraph.allow_multi_output_for_events
|
||||
) {
|
||||
graph.beforeChange()
|
||||
// @ts-expect-error Unused param
|
||||
this.disconnectOutput(slot, false, { doProcessChange: false })
|
||||
this.disconnectOutput(slot)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect } from 'vitest'
|
||||
|
||||
import { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './fixtures/testExtensions'
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LLink', () => {
|
||||
test('matches previous snapshot', () => {
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
ISerialisedGraph,
|
||||
ISerialisedNode,
|
||||
SerialisableGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
@@ -19,12 +20,7 @@ export const oldSchemaGraph: ISerialisedGraph = {
|
||||
title: 'A group to test with'
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
// @ts-expect-error TODO: Fix after merge - missing required properties for test
|
||||
{
|
||||
id: 1
|
||||
}
|
||||
],
|
||||
nodes: [{ id: 1 } as Partial<ISerialisedNode> as ISerialisedNode],
|
||||
links: []
|
||||
}
|
||||
|
||||
@@ -65,11 +61,7 @@ export const basicSerialisableGraph: SerialisableGraph = {
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
// @ts-expect-error TODO: Fix after merge - missing required properties for test
|
||||
{
|
||||
id: 1,
|
||||
type: 'mustBeSet'
|
||||
}
|
||||
{ id: 1, type: 'mustBeSet' } as Partial<ISerialisedNode> as ISerialisedNode
|
||||
],
|
||||
links: []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user