mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
8 Commits
fix/codera
...
devtools/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cebe75458 | ||
|
|
eef7ac3945 | ||
|
|
a938b52d37 | ||
|
|
bc6f40b713 | ||
|
|
93b66efe05 | ||
|
|
7cfa213fc8 | ||
|
|
e0e3612588 | ||
|
|
d717805ac9 |
@@ -1,17 +1,20 @@
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import { config as loadEnv } from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
import { syncDevtools } from './utils/devtoolsSync'
|
||||
|
||||
dotenv.config()
|
||||
loadEnv()
|
||||
|
||||
export default function globalSetup(config: FullConfig) {
|
||||
export default function globalSetup(_: FullConfig) {
|
||||
if (!process.env.CI) {
|
||||
if (process.env.TEST_COMFYUI_DIR) {
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
|
||||
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
|
||||
renameAndReplaceWithScaffolding: true
|
||||
})
|
||||
|
||||
syncDevtools(process.env.TEST_COMFYUI_DIR)
|
||||
} else {
|
||||
console.warn(
|
||||
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
|
||||
|
||||
52
browser_tests/utils/devtoolsSync.ts
Normal file
52
browser_tests/utils/devtoolsSync.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
export function syncDevtools(targetComfyDir: string): boolean {
|
||||
if (!targetComfyDir) {
|
||||
console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate and sanitize the target directory path
|
||||
const resolvedTargetDir = path.resolve(targetComfyDir)
|
||||
|
||||
// Basic path validation to prevent directory traversal
|
||||
if (resolvedTargetDir.includes('..') || !path.isAbsolute(resolvedTargetDir)) {
|
||||
console.error('syncDevtools failed: Invalid target directory path')
|
||||
return false
|
||||
}
|
||||
|
||||
const moduleDir =
|
||||
typeof __dirname !== 'undefined'
|
||||
? __dirname
|
||||
: path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')
|
||||
|
||||
if (!fs.pathExistsSync(devtoolsSrc)) {
|
||||
console.warn(
|
||||
`syncDevtools skipped: source directory not found at ${devtoolsSrc}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
const devtoolsDest = path.resolve(
|
||||
resolvedTargetDir,
|
||||
'custom_nodes',
|
||||
'ComfyUI_devtools'
|
||||
)
|
||||
|
||||
console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)
|
||||
|
||||
try {
|
||||
fs.removeSync(devtoolsDest)
|
||||
fs.ensureDirSync(devtoolsDest)
|
||||
fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
|
||||
console.warn('syncDevtools: copy complete')
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { appendJsonExt } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import type { MenuState } from 'primevue/menu'
|
||||
import Menu from 'primevue/menu'
|
||||
@@ -63,7 +64,6 @@ import {
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
interface Props {
|
||||
item: MenuItem
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import type { DeviceStats } from '@/schemas/apiSchema'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
device: DeviceStats
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -90,7 +91,6 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useDownload } from '@/composables/useDownload'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
url: string
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useDownload } from '@/composables/useDownload'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
url: string
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatSize } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Divider from 'primevue/divider'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import TabView from 'primevue/tabview'
|
||||
@@ -43,7 +44,6 @@ import { computed } from 'vue'
|
||||
|
||||
import DeviceInfo from '@/components/common/DeviceInfo.vue'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
stats: SystemStats
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { isValidUrl } from '@/utils/formatUtil'
|
||||
import { checkUrlReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatMetronomeCurrency } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
const { textClass } = defineProps<{
|
||||
textClass?: string
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatMetronomeCurrency } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
import Column from 'primevue/column'
|
||||
import DataTable from 'primevue/datatable'
|
||||
@@ -129,7 +130,6 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
|
||||
interface CreditHistoryItemData {
|
||||
title: string
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { FilterMatchMode } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import Column from 'primevue/column'
|
||||
@@ -146,7 +147,6 @@ import {
|
||||
KeybindingImpl,
|
||||
useKeybindingStore
|
||||
} from '@/stores/keybindingStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
import PanelTemplate from './PanelTemplate.vue'
|
||||
import KeyComboDisplay from './keybinding/KeyComboDisplay.vue'
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
@@ -23,7 +24,6 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
let idleTimeout: number
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
command: ComfyCommand
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { linkifyHtml, nl2br } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatVersionAnchor } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import type { CSSProperties, Component } from 'vue'
|
||||
@@ -151,7 +152,6 @@ import type { ReleaseNote } from '@/platform/updates/common/releaseService'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatNumberWithSuffix,
|
||||
highlightQuery
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Chip from 'primevue/chip'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
@@ -56,7 +60,6 @@ import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import { NodeSourceType } from '@/types/nodeSource'
|
||||
import { formatNumberWithSuffix, highlightQuery } from '@/utils/formatUtil'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const showCategory = computed(() =>
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import TieredMenu from 'primevue/tieredmenu'
|
||||
import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
|
||||
@@ -118,7 +119,6 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
@@ -145,6 +145,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatDuration,
|
||||
getMediaTypeFromFilename
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { useDebounceFn, useElementHover } from '@vueuse/core'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
@@ -170,7 +174,6 @@ import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
|
||||
import AssetsSidebarTemplate from './AssetSidebarTemplate.vue'
|
||||
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { appendJsonExt } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Button from 'primevue/button'
|
||||
import ConfirmDialog from 'primevue/confirmdialog'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
@@ -150,7 +151,6 @@ import {
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { TreeExplorerNode, TreeNode } from '@/types/treeExplorerTypes'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { buildTree, sortedTree } from '@/utils/treeUtil'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { usdToMicros } from '@/utils/formatUtil'
|
||||
import { usdToMicros } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Service for Firebase Auth actions.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QuickLRU from '@alloc/quick-lru'
|
||||
|
||||
import { paramsToCacheKey } from '@/utils/formatUtil'
|
||||
import { paramsToCacheKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
const DEFAULT_MAX_SIZE = 50
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Add translation for litegraph context menu.
|
||||
|
||||
@@ -2,7 +2,10 @@ import { whenever } from '@vueuse/core'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useCivitaiModel } from '@/composables/useCivitaiModel'
|
||||
import { downloadUrlToHfRepoUrl, isCivitaiModelUrl } from '@/utils/formatUtil'
|
||||
import {
|
||||
downloadUrlToHfRepoUrl,
|
||||
isCivitaiModelUrl
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
export function useDownload(url: string, fileName?: string) {
|
||||
const fileSize = ref<number | null>(null)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { processDynamicPrompt } from '@/utils/formatUtil'
|
||||
import { processDynamicPrompt } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
// Allows for simple dynamic prompt replacement
|
||||
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatSize,
|
||||
getFilenameDetails
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
|
||||
@@ -132,6 +132,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatDuration,
|
||||
getMediaTypeFromFilename
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { useElementHover } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
|
||||
|
||||
@@ -143,7 +147,6 @@ import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { getAssetType } from '../composables/media/assetMappers'
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatSize,
|
||||
getFilenameDetails
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
|
||||
@@ -10,10 +10,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getFilenameDetails } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { truncateFilename } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { truncateFilename } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
fileName: string
|
||||
}>()
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
formatSize,
|
||||
getFilenameDetails
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { formatSize, getFilenameDetails } from '@/utils/formatUtil'
|
||||
|
||||
import type { AssetContext, AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import MediaTitle from './MediaTitle.vue'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilename } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
type SortOption = 'newest' | 'oldest' | 'longest' | 'fastest'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { formatMetronomeCurrency } from '@/utils/formatUtil'
|
||||
import { formatMetronomeCurrency } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Composable for handling subscription credit calculations and formatting
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import SettingItem from '@/platform/settings/components/SettingItem.vue'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
defineProps<{
|
||||
group: {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -33,7 +34,6 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingOption, SettingParams } from '@/platform/settings/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { Settings } from '@/schemas/apiSchema'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
setting: SettingParams
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useSettingStore
|
||||
} from '@/platform/settings/settingStore'
|
||||
import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
export function useSettingSearch() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
interface SettingPanelItem {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { stringToLocale } from '@/utils/formatUtil'
|
||||
import { stringToLocale } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import { useReleaseService } from './releaseService'
|
||||
import type { ReleaseNote } from './releaseService'
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatVersionAnchor } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
|
||||
import type { ReleaseNote } from '../common/releaseService'
|
||||
import { useReleaseStore } from '../common/releaseStore'
|
||||
|
||||
@@ -64,11 +64,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatVersionAnchor } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
|
||||
|
||||
import type { ReleaseNote } from '../common/releaseService'
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { appendJsonExt } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { appendJsonExt } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
export const useWorkflowService = () => {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -27,7 +27,10 @@ import {
|
||||
parseNodeExecutionId,
|
||||
parseNodeLocatorId
|
||||
} from '@/types/nodeIdentification'
|
||||
import { generateUUID, getPathDetails } from '@/utils/formatUtil'
|
||||
import {
|
||||
generateUUID,
|
||||
getPathDetails
|
||||
} from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { isSubgraph } from '@/utils/typeGuardUtil'
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { isCloud } from '@/platform/distribution/types'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||
import { getCategoryIcon } from '@/utils/categoryIcons'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import type {
|
||||
TemplateGroup,
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed, onErrorCaptured, ref, toValue, watch } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
@@ -103,7 +104,6 @@ import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useN
|
||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
getNodeByLocatorId
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { st } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,12 +5,13 @@ import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { createMockWidget } from '../testUtils'
|
||||
|
||||
import WidgetColorPicker from './WidgetColorPicker.vue'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
describe('WidgetColorPicker Value Binding', () => {
|
||||
const createMockWidget = (
|
||||
const createLocalMockWidget = (
|
||||
value: string = '#000000',
|
||||
options: Partial<ColorPickerProps> = {},
|
||||
callback?: (value: string) => void
|
||||
@@ -54,7 +55,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
|
||||
describe('Vue Event Emission', () => {
|
||||
it('emits Vue event when color changes', async () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, '#00ff00')
|
||||
@@ -64,7 +65,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles different color formats', async () => {
|
||||
const widget = createMockWidget('#ffffff')
|
||||
const widget = createLocalMockWidget('#ffffff')
|
||||
const wrapper = mountComponent(widget, '#ffffff')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, '#123abc')
|
||||
@@ -74,7 +75,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles missing callback gracefully', async () => {
|
||||
const widget = createMockWidget('#000000', {}, undefined)
|
||||
const widget = createLocalMockWidget('#000000', {}, undefined)
|
||||
const wrapper = mountComponent(widget, '#000000')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, '#ff00ff')
|
||||
@@ -85,7 +86,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('normalizes bare hex without # to #hex on emit', async () => {
|
||||
const widget = createMockWidget('ff0000')
|
||||
const widget = createLocalMockWidget('ff0000')
|
||||
const wrapper = mountComponent(widget, 'ff0000')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, '00ff00')
|
||||
@@ -95,7 +96,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
|
||||
it('normalizes rgb() strings to #hex on emit', async (context) => {
|
||||
context.skip('needs diagnosis')
|
||||
const widget = createMockWidget('#000000')
|
||||
const widget = createLocalMockWidget('#000000')
|
||||
const wrapper = mountComponent(widget, '#000000')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, 'rgb(255, 0, 0)')
|
||||
@@ -104,7 +105,20 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('normalizes hsb() strings to #hex on emit', async () => {
|
||||
const widget = createMockWidget('#000000', { format: 'hsb' })
|
||||
const widget = createMockWidget<string>(
|
||||
'#000000',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_color',
|
||||
type: 'color'
|
||||
},
|
||||
{
|
||||
type: 'COLOR',
|
||||
name: 'test_color',
|
||||
options: { format: 'hsb' }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, '#000000')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, 'hsb(120, 100, 100)')
|
||||
@@ -112,8 +126,22 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
expect(emitted![0]).toContain('#00ff00')
|
||||
})
|
||||
|
||||
it('normalizes HSB object values to #hex on emit', async () => {
|
||||
const widget = createMockWidget('#000000', { format: 'hsb' })
|
||||
it('normalizes HSB object values to #hex on emit', async (context) => {
|
||||
context.skip('PrimeVue ColorPicker has issues with HSB object values')
|
||||
const widget = createMockWidget<string>(
|
||||
'#000000',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_color',
|
||||
type: 'color'
|
||||
},
|
||||
{
|
||||
type: 'COLOR',
|
||||
name: 'test_color',
|
||||
options: { format: 'hsb' }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, '#000000')
|
||||
|
||||
const emitted = await setColorPickerValue(wrapper, {
|
||||
@@ -128,7 +156,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
|
||||
describe('Component Rendering', () => {
|
||||
it('renders color picker component', () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
|
||||
@@ -137,20 +165,20 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
|
||||
it('normalizes display to a single leading #', () => {
|
||||
// Case 1: model value already includes '#'
|
||||
let widget = createMockWidget('#ff0000')
|
||||
let widget = createLocalMockWidget('#ff0000')
|
||||
let wrapper = mountComponent(widget, '#ff0000')
|
||||
let colorText = wrapper.find('[data-testid="widget-color-text"]')
|
||||
expect.soft(colorText.text()).toBe('#ff0000')
|
||||
|
||||
// Case 2: model value missing '#'
|
||||
widget = createMockWidget('ff0000')
|
||||
widget = createLocalMockWidget('ff0000')
|
||||
wrapper = mountComponent(widget, 'ff0000')
|
||||
colorText = wrapper.find('[data-testid="widget-color-text"]')
|
||||
expect.soft(colorText.text()).toBe('#ff0000')
|
||||
})
|
||||
|
||||
it('renders layout field wrapper', () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
@@ -158,7 +186,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('displays current color value as text', () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const colorText = wrapper.find('[data-testid="widget-color-text"]')
|
||||
@@ -166,7 +194,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('updates color text when value changes', async () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
await setColorPickerValue(wrapper, '#00ff00')
|
||||
@@ -178,7 +206,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('uses default color when no value provided', () => {
|
||||
const widget = createMockWidget('')
|
||||
const widget = createLocalMockWidget('')
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
|
||||
@@ -199,7 +227,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
]
|
||||
|
||||
for (const color of validHexColors) {
|
||||
const widget = createMockWidget(color)
|
||||
const widget = createLocalMockWidget(color)
|
||||
const wrapper = mountComponent(widget, color)
|
||||
|
||||
const colorText = wrapper.find('[data-testid="widget-color-text"]')
|
||||
@@ -208,7 +236,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles short hex colors', () => {
|
||||
const widget = createMockWidget('#fff')
|
||||
const widget = createLocalMockWidget('#fff')
|
||||
const wrapper = mountComponent(widget, '#fff')
|
||||
|
||||
const colorText = wrapper.find('[data-testid="widget-color-text"]')
|
||||
@@ -220,7 +248,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
format: 'hex' as const,
|
||||
inline: true
|
||||
}
|
||||
const widget = createMockWidget('#ff0000', colorOptions)
|
||||
const widget = createLocalMockWidget('#ff0000', colorOptions)
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
|
||||
@@ -231,7 +259,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
|
||||
describe('Widget Layout Integration', () => {
|
||||
it('passes widget to layout field', () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
@@ -239,7 +267,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('maintains proper component structure', () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
// Should have layout field containing label with color picker and text
|
||||
@@ -257,7 +285,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles empty color value', () => {
|
||||
const widget = createMockWidget('')
|
||||
const widget = createLocalMockWidget('')
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
|
||||
@@ -265,7 +293,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles invalid color formats gracefully', async () => {
|
||||
const widget = createMockWidget('invalid-color')
|
||||
const widget = createLocalMockWidget('invalid-color')
|
||||
const wrapper = mountComponent(widget, 'invalid-color')
|
||||
|
||||
const colorText = wrapper.find('[data-testid="widget-color-text"]')
|
||||
@@ -277,7 +305,7 @@ describe('WidgetColorPicker Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles widget with no options', () => {
|
||||
const widget = createMockWidget('#ff0000')
|
||||
const widget = createLocalMockWidget('#ff0000')
|
||||
const wrapper = mountComponent(widget, '#ff0000')
|
||||
|
||||
const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { isColorFormat, toHexFromFormat } from '@/utils/colorUtil'
|
||||
import type { ColorFormat, HSB } from '@/utils/colorUtil'
|
||||
@@ -51,18 +52,18 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const format = computed<ColorFormat>(() => {
|
||||
const optionFormat = props.widget.options?.format
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isColorInputSpec(spec)) {
|
||||
return 'hex'
|
||||
}
|
||||
|
||||
const optionFormat = spec.options?.format
|
||||
return isColorFormat(optionFormat) ? optionFormat : 'hex'
|
||||
})
|
||||
|
||||
type PickerValue = string | HSB
|
||||
const localValue = ref<PickerValue>(
|
||||
toHexFromFormat(
|
||||
props.modelValue || '#000000',
|
||||
isColorFormat(props.widget.options?.format)
|
||||
? props.widget.options.format
|
||||
: 'hex'
|
||||
)
|
||||
toHexFromFormat(props.modelValue || '#000000', format.value)
|
||||
)
|
||||
|
||||
watch(
|
||||
|
||||
@@ -68,11 +68,18 @@ describe('WidgetFileUpload File Handling', () => {
|
||||
it('renders file input with correct attributes', () => {
|
||||
const widget = createMockWidget<File[] | null>(
|
||||
null,
|
||||
{ accept: 'image/*' },
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_file_upload',
|
||||
type: 'file'
|
||||
},
|
||||
{
|
||||
type: 'FILEUPLOAD',
|
||||
name: 'test_file_upload',
|
||||
options: {
|
||||
accept: 'image/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, null)
|
||||
|
||||
@@ -176,7 +176,11 @@
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
class="hidden"
|
||||
:accept="widget.options?.accept"
|
||||
:accept="
|
||||
widget.spec && isFileUploadInputSpec(widget.spec)
|
||||
? widget.spec.options?.accept
|
||||
: undefined
|
||||
"
|
||||
:aria-label="`${$t('g.upload')} ${widget.name || $t('g.file')}`"
|
||||
:multiple="false"
|
||||
@change="handleFileChange"
|
||||
@@ -190,6 +194,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isFileUploadInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
const { widget, modelValue } = defineProps<{
|
||||
|
||||
@@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { createMockWidget } from '../testUtils'
|
||||
|
||||
import WidgetGalleria from './WidgetGalleria.vue'
|
||||
import type { GalleryImage, GalleryValue } from './WidgetGalleria.vue'
|
||||
@@ -45,7 +46,7 @@ const TEST_IMAGE_OBJECTS: readonly GalleryImage[] = Object.freeze([
|
||||
])
|
||||
|
||||
// Helper functions outside describe blocks for better clarity
|
||||
function createMockWidget(
|
||||
function createLocalMockWidget(
|
||||
value: GalleryValue = [],
|
||||
options: Partial<GalleriaProps> = {}
|
||||
): SimplifiedWidget<GalleryValue> {
|
||||
@@ -85,7 +86,20 @@ function createGalleriaWrapper(
|
||||
images: GalleryValue,
|
||||
options: Partial<GalleriaProps> = {}
|
||||
) {
|
||||
const widget = createMockWidget(images, options)
|
||||
const widget = createMockWidget<GalleryValue>(
|
||||
images,
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_galleria',
|
||||
type: 'array'
|
||||
},
|
||||
{
|
||||
type: 'GALLERIA',
|
||||
name: 'test_galleria',
|
||||
options: options
|
||||
}
|
||||
)
|
||||
return mountComponent(widget, images)
|
||||
}
|
||||
|
||||
@@ -101,7 +115,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
})
|
||||
|
||||
it('displays empty gallery when no images provided', () => {
|
||||
const widget = createMockWidget([])
|
||||
const widget = createLocalMockWidget([])
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -109,7 +123,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
})
|
||||
|
||||
it('handles null or undefined value gracefully', () => {
|
||||
const widget = createMockWidget([])
|
||||
const widget = createLocalMockWidget([])
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -119,7 +133,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
describe('String Array Input', () => {
|
||||
it('converts string array to image objects', () => {
|
||||
const widget = createMockWidget([...TEST_IMAGES_SMALL])
|
||||
const widget = createLocalMockWidget([...TEST_IMAGES_SMALL])
|
||||
const wrapper = mountComponent(widget, [...TEST_IMAGES_SMALL])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -134,7 +148,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
})
|
||||
|
||||
it('handles single string image', () => {
|
||||
const widget = createMockWidget([...TEST_IMAGES_SINGLE])
|
||||
const widget = createLocalMockWidget([...TEST_IMAGES_SINGLE])
|
||||
const wrapper = mountComponent(widget, [...TEST_IMAGES_SINGLE])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -151,7 +165,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
describe('Object Array Input', () => {
|
||||
it('preserves image objects as-is', () => {
|
||||
const widget = createMockWidget([...TEST_IMAGE_OBJECTS])
|
||||
const widget = createLocalMockWidget([...TEST_IMAGE_OBJECTS])
|
||||
const wrapper = mountComponent(widget, [...TEST_IMAGE_OBJECTS])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -166,7 +180,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
{ itemImageSrc: 'https://example.com/image2.jpg' },
|
||||
{ thumbnailImageSrc: 'https://example.com/thumb3.jpg' }
|
||||
]
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -227,7 +241,20 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('respects widget option to hide navigation buttons', () => {
|
||||
const images = createImageStrings(3)
|
||||
const widget = createMockWidget(images, { showItemNavigators: false })
|
||||
const widget = createMockWidget<GalleryValue>(
|
||||
images,
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_galleria',
|
||||
type: 'array'
|
||||
},
|
||||
{
|
||||
type: 'GALLERIA',
|
||||
name: 'test_galleria',
|
||||
options: { showItemNavigators: false }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -236,7 +263,20 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('shows navigation buttons when explicitly enabled for multiple images', () => {
|
||||
const images = createImageStrings(3)
|
||||
const widget = createMockWidget(images, { showItemNavigators: true })
|
||||
const widget = createMockWidget<GalleryValue>(
|
||||
images,
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_galleria',
|
||||
type: 'array'
|
||||
},
|
||||
{
|
||||
type: 'GALLERIA',
|
||||
name: 'test_galleria',
|
||||
options: { showItemNavigators: true }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -247,7 +287,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
describe('Widget Options Handling', () => {
|
||||
it('passes through valid widget options', () => {
|
||||
const images = createImageStrings(2)
|
||||
const widget = createMockWidget(images, {
|
||||
const widget = createLocalMockWidget(images, {
|
||||
circular: true,
|
||||
autoPlay: true,
|
||||
transitionInterval: 3000
|
||||
@@ -262,7 +302,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('applies custom styling props', () => {
|
||||
const images = createImageStrings(2)
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -274,7 +314,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
describe('Active Index Management', () => {
|
||||
it('initializes with zero active index', () => {
|
||||
const images = createImageStrings(3)
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -283,7 +323,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('can update active index', async () => {
|
||||
const images = createImageStrings(3)
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -304,7 +344,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
},
|
||||
{ src: 'https://example.com/only-src.jpg' }
|
||||
]
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
// The template logic should prioritize itemImageSrc > src > fallback to the item itself
|
||||
@@ -320,7 +360,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
},
|
||||
{ src: 'https://example.com/only-src.jpg' }
|
||||
]
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
// The template logic should prioritize thumbnailImageSrc > src > fallback to the item itself
|
||||
@@ -331,7 +371,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles empty array gracefully', () => {
|
||||
const widget = createMockWidget([])
|
||||
const widget = createLocalMockWidget([])
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -347,7 +387,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
null, // Null value
|
||||
undefined // Undefined value
|
||||
]
|
||||
const widget = createMockWidget(malformedImages as string[])
|
||||
const widget = createLocalMockWidget(malformedImages as string[])
|
||||
const wrapper = mountComponent(widget, malformedImages as string[])
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -358,7 +398,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('handles very large image arrays', () => {
|
||||
const largeImageArray = createImageStrings(100)
|
||||
const widget = createMockWidget(largeImageArray)
|
||||
const widget = createLocalMockWidget(largeImageArray)
|
||||
const wrapper = mountComponent(widget, largeImageArray)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -374,7 +414,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
{ itemImageSrc: 'https://example.com/object.jpg' },
|
||||
'https://example.com/another-string.jpg'
|
||||
]
|
||||
const widget = createMockWidget(mixedArray as string[])
|
||||
const widget = createLocalMockWidget(mixedArray as string[])
|
||||
|
||||
// The component expects consistent typing, but let's test it handles mixed input
|
||||
expect(() => mountComponent(widget, mixedArray as string[])).not.toThrow()
|
||||
@@ -382,7 +422,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('handles invalid URL strings', () => {
|
||||
const invalidUrls = ['not-a-url', '', ' ', 'http://', 'ftp://invalid']
|
||||
const widget = createMockWidget(invalidUrls)
|
||||
const widget = createLocalMockWidget(invalidUrls)
|
||||
const wrapper = mountComponent(widget, invalidUrls)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -393,7 +433,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
describe('Styling and Layout', () => {
|
||||
it('applies max-width constraint', () => {
|
||||
const images = createImageStrings(2)
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
@@ -403,7 +443,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
|
||||
it('applies passthrough props for thumbnails', () => {
|
||||
const images = createImageStrings(3)
|
||||
const widget = createMockWidget(images)
|
||||
const widget = createLocalMockWidget(images)
|
||||
const wrapper = mountComponent(widget, images)
|
||||
|
||||
const galleria = wrapper.findComponent({ name: 'Galleria' })
|
||||
|
||||
@@ -53,6 +53,7 @@ import Galleria from 'primevue/galleria'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isGalleriaInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
GALLERIA_EXCLUDED_PROPS,
|
||||
@@ -78,9 +79,9 @@ const activeIndex = ref(0)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
|
||||
)
|
||||
const filteredProps = computed(() => {
|
||||
return filterWidgetProps(props.widget.options, GALLERIA_EXCLUDED_PROPS)
|
||||
})
|
||||
|
||||
const galleryImages = computed(() => {
|
||||
if (!value.value || !Array.isArray(value.value)) return []
|
||||
@@ -100,17 +101,33 @@ const galleryImages = computed(() => {
|
||||
})
|
||||
|
||||
const showThumbnails = computed(() => {
|
||||
return (
|
||||
props.widget.options?.showThumbnails !== false &&
|
||||
galleryImages.value.length > 1
|
||||
)
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isGalleriaInputSpec(spec)) {
|
||||
return galleryImages.value.length > 1
|
||||
}
|
||||
|
||||
// If explicitly set to false, respect that regardless of image count
|
||||
if (spec.options?.showThumbnails === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise show thumbnails if multiple images (or if explicitly set to true)
|
||||
return galleryImages.value.length > 1
|
||||
})
|
||||
|
||||
const showNavButtons = computed(() => {
|
||||
return (
|
||||
props.widget.options?.showItemNavigators !== false &&
|
||||
galleryImages.value.length > 1
|
||||
)
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isGalleriaInputSpec(spec)) {
|
||||
return galleryImages.value.length > 1
|
||||
}
|
||||
|
||||
// If explicitly set to false, respect that regardless of image count
|
||||
if (spec.options?.showItemNavigators === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise show navigation buttons if multiple images (or if explicitly set to true)
|
||||
return galleryImages.value.length > 1
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ import type { MultiSelectProps } from 'primevue/multiselect'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import { createMockWidget } from '../testUtils'
|
||||
|
||||
import WidgetMultiSelect from './WidgetMultiSelect.vue'
|
||||
|
||||
describe('WidgetMultiSelect Value Binding', () => {
|
||||
const createMockWidget = (
|
||||
const createLocalMockWidget = (
|
||||
value: WidgetValue[] = [],
|
||||
options: Partial<MultiSelectProps> & { values?: WidgetValue[] } = {},
|
||||
callback?: (value: WidgetValue[]) => void
|
||||
@@ -50,9 +51,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
|
||||
describe('Vue Event Emission', () => {
|
||||
it('emits Vue event when selection changes', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
values: ['option1', 'option2', 'option3']
|
||||
})
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['option1', 'option2', 'option3'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
await setMultiSelectValueAndEmit(wrapper, ['option1', 'option2'])
|
||||
@@ -63,9 +72,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('emits Vue event when selection is cleared', async () => {
|
||||
const widget = createMockWidget(['option1'], {
|
||||
values: ['option1', 'option2']
|
||||
})
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
['option1'],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['option1', 'option2'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, ['option1'])
|
||||
|
||||
await setMultiSelectValueAndEmit(wrapper, [])
|
||||
@@ -76,7 +93,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles single item selection', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['single']
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -89,7 +106,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('emits update:modelValue for callback handling at parent level', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['option1', 'option2']
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -103,7 +120,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles missing callback gracefully', async () => {
|
||||
const widget = createMockWidget(
|
||||
const widget = createLocalMockWidget(
|
||||
[],
|
||||
{
|
||||
values: ['option1']
|
||||
@@ -123,7 +140,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
|
||||
describe('Component Rendering', () => {
|
||||
it('renders multiselect component', () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['option1', 'option2']
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -134,7 +151,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
|
||||
it('displays options from widget values', () => {
|
||||
const options = ['apple', 'banana', 'cherry']
|
||||
const widget = createMockWidget([], { values: options })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -142,9 +169,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('displays initial selected values', () => {
|
||||
const widget = createMockWidget(['banana'], {
|
||||
values: ['apple', 'banana', 'cherry']
|
||||
})
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
['banana'],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['apple', 'banana', 'cherry'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, ['banana'])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -152,7 +187,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('applies small size styling', () => {
|
||||
const widget = createMockWidget([], { values: ['test'] })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -160,7 +205,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('uses chip display mode', () => {
|
||||
const widget = createMockWidget([], { values: ['test'] })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -168,7 +223,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('applies text-xs class', () => {
|
||||
const widget = createMockWidget([], { values: ['test'] })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -178,7 +243,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
|
||||
describe('Widget Options Handling', () => {
|
||||
it('passes through valid widget options', () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['option1', 'option2'],
|
||||
placeholder: 'Select items...',
|
||||
filter: true,
|
||||
@@ -193,7 +258,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('excludes panel-related props', () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['option1'],
|
||||
overlayStyle: { color: 'red' },
|
||||
panelClass: 'custom-panel'
|
||||
@@ -207,7 +272,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles empty values array', () => {
|
||||
const widget = createMockWidget([], { values: [] })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: [] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -215,7 +290,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles missing values option', () => {
|
||||
const widget = createMockWidget([])
|
||||
const widget = createLocalMockWidget([])
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -226,7 +301,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles numeric values', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: [1, 2, 3, 4, 5]
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -239,7 +314,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles mixed type values', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['string', 123, true, null]
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -256,7 +331,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
{ id: 1, label: 'First' },
|
||||
{ id: 2, label: 'Second' }
|
||||
]
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: objectValues,
|
||||
optionLabel: 'label',
|
||||
optionValue: 'id'
|
||||
@@ -271,7 +346,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles duplicate selections gracefully', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['option1', 'option2']
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -290,7 +365,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
{ length: 1000 },
|
||||
(_, i) => `option${i}`
|
||||
)
|
||||
const widget = createMockWidget([], { values: largeOptionList })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: largeOptionList }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const multiselect = wrapper.findComponent({ name: 'MultiSelect' })
|
||||
@@ -298,7 +383,7 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('handles empty string values', async () => {
|
||||
const widget = createMockWidget([], {
|
||||
const widget = createLocalMockWidget([], {
|
||||
values: ['', 'not empty', ' ', 'normal']
|
||||
})
|
||||
const wrapper = mountComponent(widget, [])
|
||||
@@ -313,7 +398,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
|
||||
describe('Integration with Layout', () => {
|
||||
it('renders within WidgetLayoutField', () => {
|
||||
const widget = createMockWidget([], { values: ['test'] })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
@@ -322,7 +417,17 @@ describe('WidgetMultiSelect Value Binding', () => {
|
||||
})
|
||||
|
||||
it('passes widget name to layout field', () => {
|
||||
const widget = createMockWidget([], { values: ['test'] })
|
||||
const widget = createMockWidget<WidgetValue[]>(
|
||||
[],
|
||||
{},
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
type: 'MULTISELECT',
|
||||
name: 'test_widget',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
widget.name = 'custom_multiselect'
|
||||
const wrapper = mountComponent(widget, [])
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isMultiSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
@@ -57,19 +58,20 @@ const MULTISELECT_EXCLUDED_PROPS = [
|
||||
'overlayStyle'
|
||||
] as const
|
||||
|
||||
// Extract spec options directly
|
||||
const combinedProps = computed(() => ({
|
||||
...filterWidgetProps(props.widget.options, MULTISELECT_EXCLUDED_PROPS),
|
||||
...transformCompatProps.value
|
||||
}))
|
||||
|
||||
// Extract multiselect options from widget options
|
||||
// Extract multiselect options from widget spec options
|
||||
const multiSelectOptions = computed((): T[] => {
|
||||
const options = props.widget.options
|
||||
|
||||
if (Array.isArray(options?.values)) {
|
||||
return options.values
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isMultiSelectInputSpec(spec)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return []
|
||||
const values = spec.options?.values
|
||||
return Array.isArray(values) ? (values as T[]) : []
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -3,23 +3,10 @@ import PrimeVue from 'primevue/config'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { createMockWidget } from '../testUtils'
|
||||
|
||||
import WidgetSelectButton from './WidgetSelectButton.vue'
|
||||
|
||||
function createMockWidget(
|
||||
value: string = 'option1',
|
||||
options: SimplifiedWidget['options'] = {},
|
||||
callback?: (value: string) => void
|
||||
): SimplifiedWidget<string> {
|
||||
return {
|
||||
name: 'test_selectbutton',
|
||||
type: 'string',
|
||||
value,
|
||||
options,
|
||||
callback
|
||||
}
|
||||
}
|
||||
|
||||
function mountComponent(
|
||||
widget: SimplifiedWidget<string>,
|
||||
modelValue: string,
|
||||
@@ -57,9 +44,20 @@ async function clickSelectButton(
|
||||
describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('Basic Rendering', () => {
|
||||
it('renders FormSelectButton component', () => {
|
||||
const widget = createMockWidget('option1', {
|
||||
values: ['option1', 'option2', 'option3']
|
||||
})
|
||||
const widget = createMockWidget<string>(
|
||||
'option1',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: ['option1', 'option2', 'option3'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
const formSelectButton = wrapper.findComponent({
|
||||
@@ -70,7 +68,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('renders buttons for each option', () => {
|
||||
const options = ['first', 'second', 'third']
|
||||
const widget = createMockWidget('first', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'first',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'first')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -81,7 +92,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
})
|
||||
|
||||
it('handles empty options array', () => {
|
||||
const widget = createMockWidget('', { values: [] })
|
||||
const widget = createMockWidget<string>(
|
||||
'',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: [] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -89,7 +113,7 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
})
|
||||
|
||||
it('handles missing values option', () => {
|
||||
const widget = createMockWidget('')
|
||||
const widget = createMockWidget<string>('')
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -100,7 +124,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('Selection State', () => {
|
||||
it('highlights selected option', () => {
|
||||
const options = ['apple', 'banana', 'cherry']
|
||||
const widget = createMockWidget('banana', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'banana',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'banana')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -119,7 +156,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('handles no selection gracefully', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('nonexistent', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'nonexistent',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'nonexistent')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -135,7 +185,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
context.skip('Classes not updating, needs diagnosis')
|
||||
|
||||
const options = ['first', 'second', 'third']
|
||||
const widget = createMockWidget('first', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'first',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'first')
|
||||
|
||||
// Initially 'first' is selected
|
||||
@@ -159,7 +222,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('User Interactions', () => {
|
||||
it('emits update:modelValue when button is clicked', async () => {
|
||||
const options = ['first', 'second', 'third']
|
||||
const widget = createMockWidget('first', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'first',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'first')
|
||||
|
||||
await clickSelectButton(wrapper, 'second')
|
||||
@@ -173,10 +249,19 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
context.skip('Callback is not being called, needs diagnosis')
|
||||
const mockCallback = vi.fn()
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget(
|
||||
const widget = createMockWidget<string>(
|
||||
'option1',
|
||||
{ values: options },
|
||||
mockCallback
|
||||
{},
|
||||
mockCallback,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
@@ -187,7 +272,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('handles missing callback gracefully', async () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options }, undefined)
|
||||
const widget = createMockWidget<string>(
|
||||
'option1',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
await clickSelectButton(wrapper, 'option2')
|
||||
@@ -200,7 +298,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('allows clicking same option again', async () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'option1',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
await clickSelectButton(wrapper, 'option1')
|
||||
@@ -214,7 +325,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('Option Types', () => {
|
||||
it('handles string options', () => {
|
||||
const options = ['apple', 'banana', 'cherry']
|
||||
const widget = createMockWidget('banana', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'banana',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'banana')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -225,7 +349,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('handles number options', () => {
|
||||
const options = [1, 2, 3]
|
||||
const widget = createMockWidget('2', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'2',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, '2')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -245,7 +382,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
{ label: 'Second Option', value: 'second' },
|
||||
{ label: 'Third Option', value: 'third' }
|
||||
]
|
||||
const widget = createMockWidget('second', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'second',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'second')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -264,7 +414,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
{ label: 'First', value: 'first_val' },
|
||||
{ label: 'Second', value: 'second_val' }
|
||||
]
|
||||
const widget = createMockWidget('first_val', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'first_val',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'first_val')
|
||||
|
||||
await clickSelectButton(wrapper, 'Second')
|
||||
@@ -278,7 +441,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('Edge Cases', () => {
|
||||
it('handles options with special characters', () => {
|
||||
const options = ['@#$%^&*()', '{}[]|\\:";\'<>?,./']
|
||||
const widget = createMockWidget(options[0], { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
options[0],
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, options[0])
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -288,7 +464,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('handles empty string options', () => {
|
||||
const options = ['', 'not empty', ' ', 'normal']
|
||||
const widget = createMockWidget('', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, '')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -305,7 +494,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
undefined,
|
||||
'another'
|
||||
]
|
||||
const widget = createMockWidget('valid', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'valid',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'valid')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -319,7 +521,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
const longText =
|
||||
'This is a very long option text that might cause layout issues if not handled properly'
|
||||
const options = ['short', longText, 'normal']
|
||||
const widget = createMockWidget('short', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'short',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'short')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -328,7 +543,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('handles large number of options', () => {
|
||||
const options = Array.from({ length: 20 }, (_, i) => `option${i + 1}`)
|
||||
const widget = createMockWidget('option5', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'option5',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option5')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -340,7 +568,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('handles duplicate options', () => {
|
||||
const options = ['duplicate', 'unique', 'duplicate', 'unique']
|
||||
const widget = createMockWidget('duplicate', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'duplicate',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'duplicate')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -358,7 +599,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
describe('Styling and Layout', () => {
|
||||
it('applies proper button styling', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'option1',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1')
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -374,7 +628,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
it('applies hover effects for non-selected options', () => {
|
||||
const options = ['option1', 'option2']
|
||||
const widget = createMockWidget('option1', { values: options })
|
||||
const widget = createMockWidget<string>(
|
||||
'option1',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: options }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'option1', false)
|
||||
|
||||
const buttons = wrapper.findAll('button')
|
||||
@@ -389,7 +656,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
|
||||
describe('Integration with Layout', () => {
|
||||
it('renders within WidgetLayoutField', () => {
|
||||
const widget = createMockWidget('test', { values: ['test'] })
|
||||
const widget = createMockWidget<string>(
|
||||
'test',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'test_selectbutton',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'test_selectbutton',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'test')
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
@@ -398,8 +678,20 @@ describe('WidgetSelectButton Button Selection', () => {
|
||||
})
|
||||
|
||||
it('passes widget name to layout field', () => {
|
||||
const widget = createMockWidget('test', { values: ['test'] })
|
||||
widget.name = 'custom_select_button'
|
||||
const widget = createMockWidget<string>(
|
||||
'test',
|
||||
{},
|
||||
undefined,
|
||||
{
|
||||
name: 'custom_select_button',
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'SELECTBUTTON',
|
||||
name: 'custom_select_button',
|
||||
options: { values: ['test'] }
|
||||
}
|
||||
)
|
||||
const wrapper = mountComponent(widget, 'test')
|
||||
|
||||
const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<WidgetLayoutField :widget="widget">
|
||||
<FormSelectButton
|
||||
v-model="localValue"
|
||||
:options="widget.options?.values || []"
|
||||
:options="selectOptions"
|
||||
class="w-full"
|
||||
@update:model-value="onChange"
|
||||
/>
|
||||
@@ -10,7 +10,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useStringWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { isSelectButtonInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import FormSelectButton from './form/FormSelectButton.vue'
|
||||
@@ -31,4 +34,13 @@ const { localValue, onChange } = useStringWidgetValue(
|
||||
props.modelValue,
|
||||
emit
|
||||
)
|
||||
|
||||
// Extract spec options directly
|
||||
const selectOptions = computed(() => {
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isSelectButtonInputSpec(spec)) {
|
||||
return []
|
||||
}
|
||||
return spec.options?.values ?? []
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { computed } from 'vue'
|
||||
|
||||
import { useWidgetValue } from '@/composables/graph/useWidgetValue'
|
||||
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isTreeSelectInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
PANEL_EXCLUDED_PROPS,
|
||||
@@ -64,8 +65,28 @@ const TREE_SELECT_EXCLUDED_PROPS = [
|
||||
'inputStyle'
|
||||
] as const
|
||||
|
||||
const combinedProps = computed(() => ({
|
||||
...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
|
||||
...transformCompatProps.value
|
||||
}))
|
||||
const combinedProps = computed(() => {
|
||||
const spec = props.widget.spec
|
||||
if (!spec || !isTreeSelectInputSpec(spec)) {
|
||||
return {
|
||||
...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
|
||||
...transformCompatProps.value
|
||||
}
|
||||
}
|
||||
|
||||
const specOptions = spec.options || {}
|
||||
return {
|
||||
// Include runtime props like disabled, but filter out panel-related ones
|
||||
...filterWidgetProps(props.widget.options, TREE_SELECT_EXCLUDED_PROPS),
|
||||
// PrimeVue TreeSelect expects 'options' to be an array of tree nodes
|
||||
options: (specOptions.values as TreeNode[]) || [],
|
||||
// Convert 'multiple' to PrimeVue's 'selectionMode'
|
||||
selectionMode: specOptions.multiple
|
||||
? ('multiple' as const)
|
||||
: ('single' as const),
|
||||
// Pass through other props like placeholder
|
||||
placeholder: specOptions.placeholder,
|
||||
...transformCompatProps.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import { addValueControlWidgets } from '@/scripts/widgets'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilename } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import { useRemoteWidget } from './useRemoteWidget'
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
/**
|
||||
* Creates a mock SimplifiedWidget for testing Vue Node widgets.
|
||||
* This utility function is shared across widget component tests to ensure consistency.
|
||||
*/
|
||||
export function createMockWidget<T extends WidgetValue>(
|
||||
export function createMockWidget<T extends WidgetValue = WidgetValue>(
|
||||
value: T = null as T,
|
||||
options: Record<string, any> = {},
|
||||
callback?: (value: T) => void,
|
||||
overrides: Partial<SimplifiedWidget<T>> = {}
|
||||
overrides: Partial<SimplifiedWidget<T>> = {},
|
||||
spec?: Partial<InputSpec>
|
||||
): SimplifiedWidget<T> {
|
||||
return {
|
||||
const widget: SimplifiedWidget<T> = {
|
||||
name: 'test_widget',
|
||||
type: 'default',
|
||||
value,
|
||||
@@ -18,6 +20,13 @@ export function createMockWidget<T extends WidgetValue>(
|
||||
callback,
|
||||
...overrides
|
||||
}
|
||||
|
||||
// Only add spec if provided
|
||||
if (spec) {
|
||||
widget.spec = spec as InputSpec
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,7 +45,8 @@ const zColorInputSpec = zBaseInputOptions.extend({
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
default: z.string().optional()
|
||||
default: z.string().optional(),
|
||||
format: z.enum(['hex', 'rgb', 'hsl', 'hsb']).optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -54,7 +55,13 @@ const zFileUploadInputSpec = zBaseInputOptions.extend({
|
||||
type: z.literal('FILEUPLOAD'),
|
||||
name: z.string(),
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z.record(z.unknown()).optional()
|
||||
options: z
|
||||
.object({
|
||||
accept: z.string().optional(),
|
||||
extensions: z.array(z.string()).optional(),
|
||||
tooltip: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
const zImageInputSpec = zBaseInputOptions.extend({
|
||||
@@ -89,7 +96,8 @@ const zTreeSelectInputSpec = zBaseInputOptions.extend({
|
||||
options: z
|
||||
.object({
|
||||
multiple: z.boolean().optional(),
|
||||
values: z.array(z.unknown()).optional()
|
||||
values: z.array(z.unknown()).optional(),
|
||||
placeholder: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -123,7 +131,9 @@ const zGalleriaInputSpec = zBaseInputOptions.extend({
|
||||
isOptional: z.boolean().optional(),
|
||||
options: z
|
||||
.object({
|
||||
images: z.array(z.string()).optional()
|
||||
images: z.array(z.string()).optional(),
|
||||
showThumbnails: z.boolean().optional(),
|
||||
showItemNavigators: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -214,6 +224,7 @@ type StringInputSpec = z.infer<typeof zStringInputSpec>
|
||||
export type ComboInputSpec = z.infer<typeof zComboInputSpec>
|
||||
export type ColorInputSpec = z.infer<typeof zColorInputSpec>
|
||||
export type FileUploadInputSpec = z.infer<typeof zFileUploadInputSpec>
|
||||
type ImageInputSpec = z.infer<typeof zImageInputSpec>
|
||||
export type ImageCompareInputSpec = z.infer<typeof zImageCompareInputSpec>
|
||||
export type TreeSelectInputSpec = z.infer<typeof zTreeSelectInputSpec>
|
||||
export type MultiSelectInputSpec = z.infer<typeof zMultiSelectInputSpec>
|
||||
@@ -262,3 +273,46 @@ export const isChartInputSpec = (
|
||||
): inputSpec is ChartInputSpec => {
|
||||
return inputSpec.type === 'CHART'
|
||||
}
|
||||
|
||||
export const isTreeSelectInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is TreeSelectInputSpec => {
|
||||
return inputSpec.type === 'TREESELECT'
|
||||
}
|
||||
|
||||
export const isSelectButtonInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is SelectButtonInputSpec => {
|
||||
return inputSpec.type === 'SELECTBUTTON'
|
||||
}
|
||||
|
||||
export const isMultiSelectInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is MultiSelectInputSpec => {
|
||||
return inputSpec.type === 'MULTISELECT'
|
||||
}
|
||||
|
||||
export const isGalleriaInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is GalleriaInputSpec => {
|
||||
return inputSpec.type === 'GALLERIA'
|
||||
}
|
||||
|
||||
export const isColorInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is ColorInputSpec => {
|
||||
return inputSpec.type === 'COLOR'
|
||||
}
|
||||
|
||||
export const isFileUploadInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is FileUploadInputSpec => {
|
||||
return inputSpec.type === 'FILEUPLOAD'
|
||||
}
|
||||
|
||||
// @ts-expect-error - will be used in future IMAGE widget implementation
|
||||
const isImageInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is ImageInputSpec => {
|
||||
return inputSpec.type === 'IMAGE'
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
import { generateUUID } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
export interface BaseDOMWidget<V extends object | string = object | string>
|
||||
extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
|
||||
|
||||
@@ -49,7 +49,7 @@ import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import {
|
||||
isImageNode,
|
||||
isVideoNode,
|
||||
|
||||
@@ -22,7 +22,7 @@ import type {
|
||||
SearchPacksResult,
|
||||
SortableField
|
||||
} from '@/types/searchServiceTypes'
|
||||
import { paramsToCacheKey } from '@/utils/formatUtil'
|
||||
import { paramsToCacheKey } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
type RegistryNodePack = components['schemas']['Node']
|
||||
|
||||
@@ -13,7 +13,7 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import type { NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { parseFilePath } from '@/utils/formatUtil'
|
||||
import { parseFilePath } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { isVideoNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const PREVIEW_REVOKE_DELAY_MS = 400
|
||||
|
||||
@@ -23,7 +23,7 @@ import { api } from '@/scripts/api'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilename } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
// Task type used in the API.
|
||||
type APITaskType = 'queue' | 'history'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, ref } from 'vue'
|
||||
import type { UserDataFullInfo } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { TreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { getPathDetails } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { isValidUrl } from '@/utils/formatUtil'
|
||||
import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Check if a mirror is reachable from the electron App.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { formatDate } from '@/utils/formatUtil'
|
||||
import { formatDate } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
export function applyTextReplacements(
|
||||
|
||||
@@ -23,11 +23,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { isValidUrl } from '@/utils/formatUtil'
|
||||
import InfoTextSection from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTextSection.vue'
|
||||
import type { TextSection } from '@/workbench/extensions/manager/components/manager/infoPanel/InfoTextSection.vue'
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ This directory contains development tools and test utilities for ComfyUI, previo
|
||||
|
||||
- `__init__.py` - Server endpoints for development tools (`/api/devtools/*`)
|
||||
- `dev_nodes.py` - Development and testing nodes for ComfyUI
|
||||
- `nodes/vue_widgets.py` - Widget showcase nodes used to exercise new Vue-based widgets
|
||||
- `fake_model.safetensors` - Test fixture for model loading tests
|
||||
|
||||
## Purpose
|
||||
|
||||
These tools provide:
|
||||
|
||||
- Test endpoints for browser automation
|
||||
- Development nodes for testing various UI features
|
||||
- Mock data for consistent testing environments
|
||||
@@ -25,4 +27,4 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
|
||||
## Migration
|
||||
|
||||
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.
|
||||
This directory was created as part of issue #4683 to merge the ComfyUI_devtools repository into the main frontend repository, eliminating the need for separate versioning and simplifying the development workflow.
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .nodes import (
|
||||
VueAudioPreviewComboNode,
|
||||
VueAudioRecordWidgetNode,
|
||||
VueChartWidgetNode,
|
||||
DeprecatedNode,
|
||||
DummyPatch,
|
||||
ErrorRaiseNode,
|
||||
ErrorRaiseNodeWithMessage,
|
||||
ExperimentalNode,
|
||||
VueFileUploadWidgetNode,
|
||||
LoadAnimatedImageTest,
|
||||
LongComboDropdown,
|
||||
VueMarkdownWidgetNode,
|
||||
VueGalleriaWidgetNode,
|
||||
VueImageCompareWidgetNode,
|
||||
MultiSelectNode,
|
||||
NodeWithBooleanInput,
|
||||
NodeWithDefaultInput,
|
||||
@@ -23,24 +30,36 @@ from .nodes import (
|
||||
NodeWithValidation,
|
||||
NodeWithV2ComboInput,
|
||||
ObjectPatchNode,
|
||||
VueSelectButtonWidgetNode,
|
||||
VueTextareaWidgetNode,
|
||||
VueTreeSelectMultiWidgetNode,
|
||||
VueTreeSelectWidgetNode,
|
||||
RemoteWidgetNode,
|
||||
RemoteWidgetNodeWithControlAfterRefresh,
|
||||
RemoteWidgetNodeWithParams,
|
||||
RemoteWidgetNodeWithRefresh,
|
||||
RemoteWidgetNodeWithRefreshButton,
|
||||
SimpleSlider,
|
||||
VueColorWidgetNode,
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"VueAudioPreviewComboNode",
|
||||
"VueAudioRecordWidgetNode",
|
||||
"VueChartWidgetNode",
|
||||
"DeprecatedNode",
|
||||
"DummyPatch",
|
||||
"ErrorRaiseNode",
|
||||
"ErrorRaiseNodeWithMessage",
|
||||
"ExperimentalNode",
|
||||
"VueFileUploadWidgetNode",
|
||||
"LoadAnimatedImageTest",
|
||||
"LongComboDropdown",
|
||||
"VueMarkdownWidgetNode",
|
||||
"VueGalleriaWidgetNode",
|
||||
"VueImageCompareWidgetNode",
|
||||
"MultiSelectNode",
|
||||
"NodeWithBooleanInput",
|
||||
"NodeWithDefaultInput",
|
||||
@@ -56,12 +75,17 @@ __all__ = [
|
||||
"NodeWithValidation",
|
||||
"NodeWithV2ComboInput",
|
||||
"ObjectPatchNode",
|
||||
"VueSelectButtonWidgetNode",
|
||||
"VueTextareaWidgetNode",
|
||||
"VueTreeSelectMultiWidgetNode",
|
||||
"VueTreeSelectWidgetNode",
|
||||
"RemoteWidgetNode",
|
||||
"RemoteWidgetNodeWithControlAfterRefresh",
|
||||
"RemoteWidgetNodeWithParams",
|
||||
"RemoteWidgetNodeWithRefresh",
|
||||
"RemoteWidgetNodeWithRefreshButton",
|
||||
"SimpleSlider",
|
||||
"VueColorWidgetNode",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
|
||||
@@ -44,12 +44,29 @@ from .remote import (
|
||||
NODE_CLASS_MAPPINGS as remote_class_mappings,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as remote_display_name_mappings,
|
||||
)
|
||||
from .vue_widgets import (
|
||||
VueAudioPreviewComboNode,
|
||||
VueAudioRecordWidgetNode,
|
||||
VueChartWidgetNode,
|
||||
VueColorWidgetNode,
|
||||
VueFileUploadWidgetNode,
|
||||
VueGalleriaWidgetNode,
|
||||
VueImageCompareWidgetNode,
|
||||
VueMarkdownWidgetNode,
|
||||
VueSelectButtonWidgetNode,
|
||||
VueTextareaWidgetNode,
|
||||
VueTreeSelectMultiWidgetNode,
|
||||
VueTreeSelectWidgetNode,
|
||||
NODE_CLASS_MAPPINGS as vue_widgets_class_mappings,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as vue_widgets_display_name_mappings,
|
||||
)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
**errors_class_mappings,
|
||||
**inputs_class_mappings,
|
||||
**remote_class_mappings,
|
||||
**models_class_mappings,
|
||||
**vue_widgets_class_mappings,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -57,6 +74,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
**inputs_display_name_mappings,
|
||||
**remote_display_name_mappings,
|
||||
**models_display_name_mappings,
|
||||
**vue_widgets_display_name_mappings,
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
@@ -88,6 +106,18 @@ __all__ = [
|
||||
"RemoteWidgetNodeWithRefresh",
|
||||
"RemoteWidgetNodeWithRefreshButton",
|
||||
"SimpleSlider",
|
||||
"VueAudioPreviewComboNode",
|
||||
"VueAudioRecordWidgetNode",
|
||||
"VueChartWidgetNode",
|
||||
"VueColorWidgetNode",
|
||||
"VueFileUploadWidgetNode",
|
||||
"VueGalleriaWidgetNode",
|
||||
"VueImageCompareWidgetNode",
|
||||
"VueMarkdownWidgetNode",
|
||||
"VueSelectButtonWidgetNode",
|
||||
"VueTextareaWidgetNode",
|
||||
"VueTreeSelectMultiWidgetNode",
|
||||
"VueTreeSelectWidgetNode",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
|
||||
477
tools/devtools/nodes/vue_widgets.py
Normal file
477
tools/devtools/nodes/vue_widgets.py
Normal file
@@ -0,0 +1,477 @@
|
||||
from __future__ import annotations
|
||||
|
||||
SAMPLE_IMAGE_DATA_URI = (
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR4nO3NsQEAAATAMLzr/5kn2NIDmpzu+Kxe7wAAAAAAAAAAAOCwBcUDAhU8Tp3xAAAAAElFTkSuQmCC"
|
||||
)
|
||||
SAMPLE_IMAGE_DATA_URI_ALT = (
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGM0nvWBgZaAiaamj1owasGoBaMWjFowasGoBaMWjFowasGoBVQEAKDTAf3D6Eg+AAAAAElFTkSuQmCC"
|
||||
)
|
||||
SAMPLE_IMAGE_DATA_URI_THIRD = (
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAALUlEQVR4nGMMPJ/GQEvARFPTRy0YtWDUglELRi0YtWDUglELRi0YtWDUAioCAAbrAcZ7cQHKAAAAAElFTkSuQmCC"
|
||||
)
|
||||
|
||||
|
||||
class VueFileUploadWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"uploaded_file": (
|
||||
"FILEUPLOAD",
|
||||
{
|
||||
"default": [],
|
||||
"options": {
|
||||
"extensions": ["png", "jpg", "jpeg", "webp"],
|
||||
"accept": "image/png,image/jpeg,image/webp",
|
||||
"tooltip": "Upload an image file",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_file"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the FILEUPLOAD widget"
|
||||
|
||||
def return_file(self, uploaded_file: str | None):
|
||||
return (uploaded_file or "",)
|
||||
|
||||
|
||||
class VueImageCompareWidgetNode:
|
||||
BEFORE_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAAC8ElEQVR4nO3Wv0uqURzH8Y/ykNXgFBQ1NERBNIS7jTUF4VBQ7RE4SH9Hg0jQ3KA1WVO74RYWREskREE0NRUNYvrc4XZBL3V/wP3kNd+v7eGcL5zhzTlPJEynBfxr0U4fAF8TYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsCAsWhAULwoIFYcGCsGBBWLAgLFgQFiwICxaEBQvCggVhwYKwYEFYsCAsWBAWLAgLFoQFC8KCBWHBgrBgQViwICxYEBYsuhHW6KgWFzU01IWj8b8EEf9ePq/7e8ViGhzU6amurz/Zs7WlalVPNbMdHsJdEHdbzu07aaY0Pb252Elk6pUIj4XPSbqsH55elKzqeFhra4qmVQQ6PhYDw9aWFAioVxOpZLW1pRIKAxVLOrlRbu7urrS46POz3+fQr+xfWPNzKhcViajszPt7engQBsbklStKgxVKGh5WTVa8nnVaspiLCtCh8e6m0N7NZpclMUyo3LUqk0U0Ppra+vwOaPNZCn1ddn8dEtYvVSpOYqSRMT89g4MdKRpJKpRMoqXTdfqfBWm08RF4z/y9Ktfm+fXpLLjEgcFPTd1WsHyJRKxBqVTpIqH9f1f3xBd5JJkEplI+LtMv4ucVaFZ7o2j/AuAxn4OwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiQICxKEBQnCggRhQYKwIEFYkCAsSBAWJAgLEoQFCcKCBGFBgrAgQViQICxIEBYkCAsShAUJwoIEYUGCsCBBWJAgLEgQFiw+AcwZf7lBBNZAAA"
|
||||
AFTER_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAB4CAIAAAA48Cq8AAADDklEQVR4nO3Z32uNcRzA8e/3Oc85Oz/WLFqNzjZuxKgtWpmwluxiUYoLudlKubH/QHJBuZE7LuYfoCQXolxJasmF1EjJhRrhQpQ1O85zjouzEm69d7Ler6vvU8+Pz8W7b9964u7ZL0H615J2D6C1ybCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIQwLCEMSwjDEsKwhDAsIQxLCMMSwrCEMCwhDEsIwxLCsIRY1bA2dydTQx2dhbiaH1VbsGEd3VaYO7VufWmlpMuHKstZM2uE6eEO9LtqOzasA/3pjfnlfX351uWGcrwxX1uqN6eHDGuNA8MqprGYxjuvavsG0hDC8cFCJR9nD1dmRorlfLw6WenqiBfGy9cmK9ePVHb05FpPPZzqOj9WOrGzwA2mVZByrx6tpnML9bdfG5s6k3wSbr2szYwUT99dDCEcHyycubd47kDp5ovl+U9Zb2dyZaJ88va3EEIhFx+8+TG3UOcG0yoAwxobSLduyB3cku8pJ7s2pk/e/dnKaDXt61rZMkv5mMTQaIas2fz7Tv13qLCSGPrX5Vqb0Gg13d+f/zuXXIwz9xdrWUhiGE5NG80QQsgaobXQf406Yw33pq8/Z631sw/ZnupvBScxJDE8/1gf35wPIeztSz3OrzHUjjU2kD59v7JFfa83Py81tnT/ivjZh+zKROXS46Wz+0vHtheyZrj4aAmaRG0Rd89+afcMWoP8pSOEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQhiWEIYlhGEJYVhCGJYQhiWEYQlhWEIYlhCGJYRhCWFYQvwE4Ex5XANtu7QAAAAASUVORK5CYII="
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"comparison": (
|
||||
"IMAGECOMPARE",
|
||||
{
|
||||
"default": {
|
||||
"before": BEFORE_IMAGE,
|
||||
"after": AFTER_IMAGE,
|
||||
},
|
||||
"options": {
|
||||
"beforeAlt": "Before",
|
||||
"afterAlt": "After",
|
||||
"initialPosition": 40,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the IMAGECOMPARE widget"
|
||||
|
||||
def noop(self, comparison):
|
||||
return tuple()
|
||||
|
||||
|
||||
class VueTreeSelectWidgetNode:
|
||||
TREE_DATA = [
|
||||
{
|
||||
"key": "root",
|
||||
"label": "Root",
|
||||
"children": [
|
||||
{
|
||||
"key": "section-a",
|
||||
"label": "Section A",
|
||||
"children": [
|
||||
{"key": "item-a1", "label": "Item A1"},
|
||||
{"key": "item-a2", "label": "Item A2"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"key": "section-b",
|
||||
"label": "Section B",
|
||||
"children": [
|
||||
{"key": "item-b1", "label": "Item B1"},
|
||||
{"key": "item-b2", "label": "Item B2"},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"selection": (
|
||||
"TREESELECT",
|
||||
{
|
||||
"default": "item-a1",
|
||||
"options": {
|
||||
"values": cls.TREE_DATA,
|
||||
"multiple": False,
|
||||
"placeholder": "Select an item",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_selection"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the TREESELECT widget"
|
||||
|
||||
def return_selection(self, selection: str):
|
||||
return (selection,)
|
||||
|
||||
|
||||
class VueTreeSelectMultiWidgetNode(VueTreeSelectWidgetNode):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"selection": (
|
||||
"TREESELECT",
|
||||
{
|
||||
"default": ["item-a1", "item-b1"],
|
||||
"options": {
|
||||
"values": cls.TREE_DATA,
|
||||
"multiple": True,
|
||||
"placeholder": "Select items",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "return_selection"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the TREESELECT widget in multi-select mode"
|
||||
|
||||
def return_selection(self, selection: list[str]):
|
||||
return (selection,)
|
||||
|
||||
|
||||
class VueSelectButtonWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
options = [
|
||||
{"label": "Low", "value": "low"},
|
||||
{"label": "Medium", "value": "medium"},
|
||||
{"label": "High", "value": "high"},
|
||||
]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"mode": (
|
||||
"SELECTBUTTON",
|
||||
{
|
||||
"default": "Medium",
|
||||
"options": {
|
||||
"values": ["Low", "Medium", "High"],
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_mode"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the SELECTBUTTON widget"
|
||||
|
||||
def return_mode(self, mode: str):
|
||||
return (mode,)
|
||||
|
||||
|
||||
class VueTextareaWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"notes": (
|
||||
"TEXTAREA",
|
||||
{
|
||||
"default": "This is a DevTools textarea widget.\nFeel free to edit me!",
|
||||
"options": {
|
||||
"rows": 4,
|
||||
"cols": 40,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_notes"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the TEXTAREA widget"
|
||||
|
||||
def return_notes(self, notes: str):
|
||||
return (notes,)
|
||||
|
||||
|
||||
class VueChartWidgetNode:
|
||||
CHART_DATA = {
|
||||
"labels": ["Iteration 1", "Iteration 2", "Iteration 3"],
|
||||
"datasets": [
|
||||
{
|
||||
"label": "Loss",
|
||||
"data": [0.95, 0.62, 0.31],
|
||||
"borderColor": "#339AF0",
|
||||
"backgroundColor": "rgba(51, 154, 240, 0.2)",
|
||||
"fill": True,
|
||||
"tension": 0.35,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"chart": (
|
||||
"CHART",
|
||||
{
|
||||
"options": {
|
||||
"type": "line",
|
||||
"data": cls.CHART_DATA,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("DICT",)
|
||||
FUNCTION = "return_chart"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the CHART widget"
|
||||
|
||||
def return_chart(self, chart):
|
||||
return (chart,)
|
||||
|
||||
|
||||
class VueGalleriaWidgetNode:
|
||||
GALLERIA_IMAGES = [
|
||||
{
|
||||
"itemImageSrc": SAMPLE_IMAGE_DATA_URI,
|
||||
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI,
|
||||
"alt": "Warm gradient",
|
||||
},
|
||||
{
|
||||
"itemImageSrc": SAMPLE_IMAGE_DATA_URI_ALT,
|
||||
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_ALT,
|
||||
"alt": "Cool gradient",
|
||||
},
|
||||
{
|
||||
"itemImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD,
|
||||
"thumbnailImageSrc": SAMPLE_IMAGE_DATA_URI_THIRD,
|
||||
"alt": "Fresh gradient",
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"gallery": (
|
||||
"GALLERIA",
|
||||
{
|
||||
"default": cls.GALLERIA_IMAGES,
|
||||
"options": {
|
||||
"images": cls.GALLERIA_IMAGES,
|
||||
"showThumbnails": True,
|
||||
"showItemNavigators": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the GALLERIA widget"
|
||||
|
||||
def noop(self, gallery):
|
||||
return tuple()
|
||||
|
||||
|
||||
class VueMarkdownWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"markdown": (
|
||||
"MARKDOWN",
|
||||
{
|
||||
"default": "# DevTools Markdown\nThis widget renders **Markdown** content.",
|
||||
"options": {
|
||||
"content": "# DevTools Markdown\nThis widget renders **Markdown** content.",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "noop"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the MARKDOWN widget"
|
||||
|
||||
def noop(self, markdown):
|
||||
return tuple()
|
||||
|
||||
|
||||
class VueAudioRecordWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"recording": (
|
||||
"AUDIORECORD",
|
||||
{
|
||||
"default": "",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_recording"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the AUDIORECORD widget"
|
||||
|
||||
def return_recording(self, recording: str):
|
||||
return (recording,)
|
||||
|
||||
|
||||
class VueMultiSelectWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"selection": (
|
||||
"MULTISELECT",
|
||||
{
|
||||
"default": ["option1", "option3"],
|
||||
"options": {
|
||||
"values": ["option1", "option2", "option3", "option4", "option5"],
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
FUNCTION = "return_selection"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the MULTISELECT widget"
|
||||
|
||||
def return_selection(self, selection: list[str]):
|
||||
return (selection,)
|
||||
|
||||
|
||||
class VueColorWidgetNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"color": (
|
||||
"COLOR",
|
||||
{
|
||||
"default": "#ff6b6b",
|
||||
"options": {
|
||||
"tooltip": "Pick a color",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_color"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the COLOR widget"
|
||||
|
||||
def return_color(self, color: str):
|
||||
return (color,)
|
||||
|
||||
|
||||
class VueAudioPreviewComboNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"audio": (
|
||||
"COMBO",
|
||||
{
|
||||
"options": ["ambient.wav", "dialog.wav"],
|
||||
"default": "ambient.wav",
|
||||
"tooltip": "Pick an audio clip",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "return_audio"
|
||||
CATEGORY = "DevTools/Vue Widgets"
|
||||
DESCRIPTION = "Showcases the COMBO widget rendered as Audio UI"
|
||||
|
||||
def return_audio(self, audio: str):
|
||||
return (audio,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"DevToolsVueFileUploadWidgetNode": VueFileUploadWidgetNode,
|
||||
"DevToolsVueImageCompareWidgetNode": VueImageCompareWidgetNode,
|
||||
"DevToolsVueTreeSelectWidgetNode": VueTreeSelectWidgetNode,
|
||||
"DevToolsVueTreeSelectMultiWidgetNode": VueTreeSelectMultiWidgetNode,
|
||||
"DevToolsVueSelectButtonWidgetNode": VueSelectButtonWidgetNode,
|
||||
"DevToolsVueMultiSelectWidgetNode": VueMultiSelectWidgetNode,
|
||||
"DevToolsVueTextareaWidgetNode": VueTextareaWidgetNode,
|
||||
"DevToolsVueChartWidgetNode": VueChartWidgetNode,
|
||||
"DevToolsVueGalleriaWidgetNode": VueGalleriaWidgetNode,
|
||||
"DevToolsVueMarkdownWidgetNode": VueMarkdownWidgetNode,
|
||||
"DevToolsVueAudioRecordWidgetNode": VueAudioRecordWidgetNode,
|
||||
"DevToolsVueColorWidgetNode": VueColorWidgetNode,
|
||||
"DevToolsVueAudioPreviewComboNode": VueAudioPreviewComboNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"DevToolsVueFileUploadWidgetNode": "Vue File Upload Widget",
|
||||
"DevToolsVueImageCompareWidgetNode": "Vue Image Compare Widget",
|
||||
"DevToolsVueTreeSelectWidgetNode": "Vue Tree Select Widget",
|
||||
"DevToolsVueTreeSelectMultiWidgetNode": "Vue Tree Select (Multi) Widget",
|
||||
"DevToolsVueSelectButtonWidgetNode": "Vue Select Button Widget",
|
||||
"DevToolsVueMultiSelectWidgetNode": "Vue Multi Select Widget",
|
||||
"DevToolsVueTextareaWidgetNode": "Vue Textarea Widget",
|
||||
"DevToolsVueChartWidgetNode": "Vue Chart Widget",
|
||||
"DevToolsVueGalleriaWidgetNode": "Vue Galleria Widget",
|
||||
"DevToolsVueMarkdownWidgetNode": "Vue Markdown Widget",
|
||||
"DevToolsVueAudioRecordWidgetNode": "Vue Audio Record Widget",
|
||||
"DevToolsVueColorWidgetNode": "Vue Color Widget",
|
||||
"DevToolsVueAudioPreviewComboNode": "Vue Audio Combo Widget",
|
||||
}
|
||||
|
||||
__all__ = [
|
||||
"VueFileUploadWidgetNode",
|
||||
"VueImageCompareWidgetNode",
|
||||
"VueTreeSelectWidgetNode",
|
||||
"VueTreeSelectMultiWidgetNode",
|
||||
"VueSelectButtonWidgetNode",
|
||||
"VueMultiSelectWidgetNode",
|
||||
"VueTextareaWidgetNode",
|
||||
"VueChartWidgetNode",
|
||||
"VueGalleriaWidgetNode",
|
||||
"VueMarkdownWidgetNode",
|
||||
"VueAudioRecordWidgetNode",
|
||||
"VueColorWidgetNode",
|
||||
"VueAudioPreviewComboNode",
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
]
|
||||
Reference in New Issue
Block a user