Compare commits

..

1 Commits

Author SHA1 Message Date
Terry Jia
54107980c4 fix: remove unnecessary allowJs from tsconfig 2026-02-10 22:00:29 -05:00
689 changed files with 4756 additions and 6066 deletions

View File

@@ -21,7 +21,6 @@
"eslint",
"import",
"oxc",
"promise",
"typescript",
"unicorn",
"vitest",
@@ -29,12 +28,6 @@
],
"rules": {
"no-async-promise-executor": "off",
"no-else-return": [
"error",
{
"allowElseIf": false
}
],
"no-console": [
"error",
{
@@ -42,29 +35,8 @@
}
],
"no-control-regex": "off",
"eqeqeq": [
"error",
"always",
{
"null": "ignore"
}
],
"func-style": [
"error",
"declaration",
{
"allowArrowFunctions": true
}
],
"no-eval": "off",
"no-new-func": "error",
// TODO: Enable and fix 104 violations
"no-param-reassign": "off",
"no-redeclare": "error",
"no-return-assign": ["error", "always"],
"no-throw-literal": "error",
"no-useless-constructor": "error",
"no-var": "error",
"no-restricted-imports": [
"error",
{
@@ -92,66 +64,15 @@
]
}
],
"no-unneeded-ternary": [
"error",
{
"defaultAssignment": false
}
],
"no-useless-call": "error",
"no-useless-concat": "error",
"prefer-const": "error",
// TODO: Enable and fix 581 violations
"prefer-destructuring": "off",
"prefer-object-has-own": "error",
"prefer-object-spread": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"promise/no-nesting": "error",
"promise/param-names": "error",
// TODO: Enable and fix 76 violations
"promise/prefer-await-to-callbacks": "off",
// TODO: Enable and fix 91 violations
"promise/prefer-await-to-then": "off",
"promise/prefer-catch": "error",
"preserve-caught-error": "error",
"yoda": [
"error",
"never",
{
"exceptRange": true
}
],
"no-self-assign": "allow",
"no-unused-expressions": "off",
"no-unused-private-class-members": "off",
"no-useless-rename": "off",
"operator-assignment": ["error", "always"],
"import/default": "error",
"import/export": "error",
"import/first": ["error", "absolute-first"],
"import/namespace": "error",
"import/no-duplicates": "error",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"vitest/consistent-each-for": [
"error",
{
"test": "for",
"describe": "for"
}
],
"vitest/consistent-test-filename": [
"error",
{
"pattern": ".*\\.test\\.ts$"
}
],
"vitest/consistent-vitest-vi": "error",
"vitest/warn-todo": "warn",
"vitest/hoisted-apis-on-top": "error",
"vitest/no-conditional-tests": "error",
"vitest/prefer-describe-function-title": "error",
"jest/expect-expect": "off",
"jest/no-conditional-expect": "off",
"jest/no-disabled-tests": "off",
@@ -161,55 +82,11 @@
"typescript/no-unnecessary-parameter-property-assignment": "off",
"typescript/no-unsafe-declaration-merging": "off",
"typescript/no-unused-vars": "off",
"unicorn/catch-error-name": [
"error",
{
"ignore": ["^error\\w+$"]
}
],
// TODO: Enable and fix 147 violations
"unicorn/consistent-function-scoping": "off",
"unicorn/error-message": "error",
"unicorn/no-abusive-eslint-disable": "error",
// TODO: Enable and fix 165 violations
"unicorn/no-array-for-each": "off",
"unicorn/no-immediate-mutation": "error",
"unicorn/no-instanceof-array": "error",
"unicorn/no-length-as-slice-end": "error",
"unicorn/no-lonely-if": "error",
"unicorn/no-negation-in-equality-check": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/prefer-math-min-max": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/no-empty-file": "off",
"unicorn/no-new-array": "off",
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-array-find": "error",
"unicorn/no-useless-undefined": [
"error",
{
"checkArguments": false,
"checkArrowFunctionBody": false
}
],
"unicorn/prefer-classlist-toggle": "error",
"unicorn/no-single-promise-in-promise-methods": "off",
"unicorn/no-this-assignment": "error",
"unicorn/no-useless-collection-argument": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-useless-fallback-in-spread": "off",
"unicorn/no-useless-spread": "off",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-spread": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-string-replace-all": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-type-error": "error",
"unicorn/throw-new-error": "error",
"typescript/await-thenable": "off",
"typescript/no-base-to-string": "off",
"typescript/no-duplicate-type-constituents": "off",
@@ -219,12 +96,6 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
// TODO: Enable and fix 372 violations (use { "ignoreConditionalTests": true })
"typescript/prefer-nullish-coalescing": "off",
// TODO: Enable and fix violations
"typescript/prefer-optional-chain": "off",
"typescript/prefer-ts-expect-error": "error",
"vue/define-props-destructuring": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},
@@ -243,8 +114,7 @@
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error",
"vitest/consistent-test-filename": "off"
"unicorn/no-empty-file": "error"
}
}
]

View File

@@ -98,10 +98,12 @@ const config: StorybookConfig = {
},
build: {
rolldownOptions: {
experimental: {
strictExecutionOrder: true
},
treeshake: false,
output: {
keepNames: true,
strictExecutionOrder: true
keepNames: true
},
onwarn: (warning, warn) => {
// Suppress specific warnings

View File

@@ -5,6 +5,13 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Subgraph duplicate ID remapping', { tag: ['@subgraph'] }, () => {
const WORKFLOW = 'subgraphs/subgraph-nested-duplicate-ids'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.Graph.DeduplicateSubgraphNodeIds',
true
)
})
test('All node IDs are globally unique after loading', async ({
comfyPage
}) => {

View File

@@ -61,10 +61,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
const [vaeEncodeNode] = await comfyPage.nodeOps.getNodeRefsByType(
'VAEEncode',
true
)
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
@@ -80,10 +77,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
const [vaeEncodeNode] = await comfyPage.nodeOps.getNodeRefsByType(
'VAEEncode',
true
)
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0)
await comfyPage.nextFrame()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -12,18 +12,6 @@ This guide covers patterns and examples for testing Vue components in the ComfyU
6. [Asynchronous Component Testing](#asynchronous-component-testing)
7. [Working with Vue Reactivity](#working-with-vue-reactivity)
## Describe Block Naming
Use `Component.__name ?? 'ComponentName'` for the top-level `describe` title. This passes the function reference (satisfying the `prefer-describe-function-title` lint rule) while providing a readable fallback:
```typescript
import MyComponent from './MyComponent.vue'
describe(MyComponent.__name ?? 'MyComponent', () => {
// ...
})
```
## Basic Component Testing
Basic approach to testing a component's rendering and structure:
@@ -33,7 +21,7 @@ Basic approach to testing a component's rendering and structure:
import { mount } from '@vue/test-utils'
import SidebarIcon from './SidebarIcon.vue'
describe(SidebarIcon.__name ?? 'SidebarIcon', () => {
describe('SidebarIcon', () => {
const exampleProps = {
icon: 'pi pi-cog',
selected: false

View File

@@ -138,10 +138,6 @@ export default defineConfig([
'import-x/no-useless-path-segments': 'error',
'import-x/no-relative-packages': 'error',
'unused-imports/no-unused-imports': 'error',
'vue/return-in-computed-property': [
'error',
{ treatUndefinedAsUnspecified: false }
],
'vue/no-v-html': 'off',
// Prohibit dark-theme: and dark: prefixes
'vue/no-restricted-class': ['error', '/^dark(-theme)?:/'],

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.40.0",
"version": "1.39.12",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",
@@ -193,7 +193,7 @@
},
"pnpm": {
"overrides": {
"vite": "catalog:"
"vite": "^8.0.0-beta.8"
}
}
}

1103
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -69,9 +69,9 @@ catalog:
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 22.2.6
oxfmt: ^0.31.0
oxlint: ^1.46.0
oxlint-tsgolint: ^0.11.5
oxfmt: ^0.26.0
oxlint: ^1.33.0
oxlint-tsgolint: ^0.9.1
picocolors: ^1.1.1
pinia: ^3.0.4
postcss-html: ^1.8.0
@@ -92,7 +92,7 @@ catalog:
unplugin-icons: ^22.5.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^30.0.0
vite: 8.0.0-beta.13
vite: ^8.0.0-beta.8
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
vite-plugin-vue-devtools: ^8.0.0

View File

@@ -33,7 +33,6 @@ fi
EXCLUDE_PATTERNS=(
'**/tsconfig*.json'
'.oxlintrc.json'
)
if [ -n "${JSON_LINT_EXCLUDES:-}" ]; then

View File

@@ -16,12 +16,12 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { electronAPI } from '@/utils/envUtil'
import { isDesktop } from '@/platform/distribution/types'
import { app } from '@/scripts/app'
const workspaceStore = useWorkspaceStore()
app.extensionManager = useWorkspaceStore()

View File

@@ -49,7 +49,7 @@ describe('downloadUtil', () => {
vi.unstubAllGlobals()
})
describe(downloadFile, () => {
describe('downloadFile', () => {
it('should create and trigger download with basic URL', () => {
const testUrl = 'https://example.com/image.png'
@@ -285,7 +285,7 @@ describe('downloadUtil', () => {
})
})
describe(extractFilenameFromContentDisposition, () => {
describe('extractFilenameFromContentDisposition', () => {
it('returns null for null header', () => {
expect(extractFilenameFromContentDisposition(null)).toBeNull()
})

View File

@@ -172,17 +172,19 @@ const splitterRefreshKey = computed(() => {
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`
})
const firstPanelStyle = computed(() =>
sidebarLocation.value === 'left'
? { display: sidebarPanelVisible.value ? 'flex' : 'none' }
: undefined
)
const firstPanelStyle = computed(() => {
if (sidebarLocation.value === 'left') {
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
}
return undefined
})
const lastPanelStyle = computed(() =>
sidebarLocation.value === 'right'
? { display: sidebarPanelVisible.value ? 'flex' : 'none' }
: undefined
)
const lastPanelStyle = computed(() => {
if (sidebarLocation.value === 'right') {
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
}
return undefined
})
</script>
<style scoped>

View File

@@ -108,7 +108,7 @@ function createTask(id: string, status: JobStatus): TaskItemImpl {
return new TaskItemImpl(createJob(id, status))
}
describe(TopMenuSection.__name ?? 'TopMenuSection', () => {
describe('TopMenuSection', () => {
beforeEach(() => {
vi.resetAllMocks()
localStorage.clear()
@@ -242,7 +242,7 @@ describe(TopMenuSection.__name ?? 'TopMenuSection', () => {
vi.mocked(settingStore.get).mockImplementation((key) => {
if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled
if (key === 'Comfy.UseNewMenu') return 'Top'
return
return undefined
})
}

View File

@@ -287,8 +287,9 @@ const openCustomNodeManager = async () => {
} catch (error) {
try {
toastErrorHandler(error)
} catch (error) {
} catch (toastError) {
console.error(error)
console.error(toastError)
}
}
}

View File

@@ -54,7 +54,7 @@ vi.mock('primevue/progressspinner', () => ({
default: { template: '<div class="progress-spinner" />' }
}))
describe(WorkspaceAuthGate.__name ?? 'WorkspaceAuthGate', () => {
describe('WorkspaceAuthGate', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsCloud.value = true

View File

@@ -51,7 +51,7 @@ vi.mock('@/stores/commandStore', () => ({
})
}))
describe(EssentialsPanel.__name ?? 'EssentialsPanel', () => {
describe('EssentialsPanel', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

View File

@@ -23,7 +23,7 @@ vi.mock('vue-i18n', () => ({
})
}))
describe(ShortcutsList.__name ?? 'ShortcutsList', () => {
describe('ShortcutsList', () => {
const mockCommands: ComfyCommandImpl[] = [
{
id: 'Workflow.New',

View File

@@ -106,7 +106,7 @@ const mountBaseTerminal = () => {
})
}
describe(BaseTerminal.__name ?? 'BaseTerminal', () => {
describe('BaseTerminal', () => {
let wrapper: VueWrapper<InstanceType<typeof BaseTerminal>> | undefined
beforeEach(() => {

View File

@@ -62,8 +62,8 @@ const terminalCreated = (
onMounted(async () => {
try {
await loadLogEntries()
} catch (error) {
console.error('Error loading logs', error)
} catch (err) {
console.error('Error loading logs', err)
// On older backends the endpoints won't exist
errorMessage.value =
'Unable to load logs, please ensure you have updated your ComfyUI backend.'

View File

@@ -78,7 +78,9 @@ interface Props {
isActive?: boolean
}
const { item, isActive = false } = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
isActive: false
})
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
@@ -101,7 +103,7 @@ const rename = async (
) => {
if (newName && newName !== initialName) {
// Synchronize the node titles with the new name
item.updateTitle?.(newName)
props.item.updateTitle?.(newName)
if (workflowStore.activeSubgraph) {
workflowStore.activeSubgraph.name = newName
@@ -125,13 +127,13 @@ const rename = async (
}
}
const isRoot = item.key === 'root'
const isRoot = props.item.key === 'root'
const tooltipText = computed(() => {
if (hasMissingNodes.value && isRoot) {
return t('breadcrumbsMenu.missingNodesWarning')
}
return item.label
return props.item.label
})
const startRename = async () => {
@@ -143,7 +145,7 @@ const startRename = async () => {
}
isEditing.value = true
itemLabel.value = item.label as string
itemLabel.value = props.item.label as string
void nextTick(() => {
if (itemInputRef.value?.$el) {
itemInputRef.value.$el.focus()
@@ -163,12 +165,12 @@ const handleClick = (event: MouseEvent) => {
}
if (event.detail === 1) {
if (isActive) {
if (props.isActive) {
menu.value?.toggle(event)
} else {
item.command?.({ item, originalEvent: event })
props.item.command?.({ item: props.item, originalEvent: event })
}
} else if (isActive && event.detail === 2) {
} else if (props.isActive && event.detail === 2) {
menu.value?.hide()
event.stopPropagation()
event.preventDefault()
@@ -178,7 +180,7 @@ const handleClick = (event: MouseEvent) => {
const inputBlur = async (doRename: boolean) => {
if (doRename) {
await rename(itemLabel.value, item.label as string)
await rename(itemLabel.value, props.item.label as string)
}
isEditing.value = false

View File

@@ -7,128 +7,123 @@ import { createApp, nextTick } from 'vue'
import ColorCustomizationSelector from './ColorCustomizationSelector.vue'
describe(
ColorCustomizationSelector.__name ?? 'ColorCustomizationSelector',
() => {
const colorOptions = [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
]
describe('ColorCustomizationSelector', () => {
const colorOptions = [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
]
beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})
beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions,
...props
}
})
}
it('renders predefined color options and custom option', () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('options')).toHaveLength(
colorOptions.length + 1
)
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
})
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})
it('initializes with custom color when non-predefined color provided', async () => {
const wrapper = mountComponent({
modelValue: '#123456'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
const colorPicker = wrapper.findComponent(ColorPicker)
expect(selectButton.props('modelValue').name).toBe('_custom')
expect(colorPicker.props('modelValue')).toBe('123456')
})
it('shows color picker when custom option is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
})
it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
await selectButton.setValue(colorOptions[0])
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})
it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})
it('inherits color from previous selection when switching to custom', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// First select a predefined color
await selectButton.setValue(colorOptions[0])
// Then switch to custom
await selectButton.setValue({ name: '_custom', value: '' })
const colorPicker = wrapper.findComponent(ColorPicker)
expect(colorPicker.props('modelValue')).toBe('0d6efd')
})
it('handles null modelValue correctly', async () => {
const wrapper = mountComponent({
modelValue: null
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: '_custom',
value: ''
})
const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions,
...props
}
})
}
)
it('renders predefined color options and custom option', () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('options')).toHaveLength(colorOptions.length + 1)
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
})
it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})
it('initializes with custom color when non-predefined color provided', async () => {
const wrapper = mountComponent({
modelValue: '#123456'
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
const colorPicker = wrapper.findComponent(ColorPicker)
expect(selectButton.props('modelValue').name).toBe('_custom')
expect(colorPicker.props('modelValue')).toBe('123456')
})
it('shows color picker when custom option is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
})
it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
await selectButton.setValue(colorOptions[0])
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})
it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })
// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})
it('inherits color from previous selection when switching to custom', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)
// First select a predefined color
await selectButton.setValue(colorOptions[0])
// Then switch to custom
await selectButton.setValue({ name: '_custom', value: '' })
const colorPicker = wrapper.findComponent(ColorPicker)
expect(colorPicker.props('modelValue')).toBe('0d6efd')
})
it('handles null modelValue correctly', async () => {
const wrapper = mountComponent({
modelValue: null
})
await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: '_custom',
value: ''
})
})
})

View File

@@ -5,7 +5,7 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
const { renderFunction } = defineProps<{
const props = defineProps<{
renderFunction: () => HTMLElement
}>()
@@ -14,12 +14,12 @@ const container = ref<HTMLElement | null>(null)
function renderContent() {
if (container.value) {
container.value.innerHTML = ''
const element = renderFunction()
const element = props.renderFunction()
container.value.appendChild(element)
}
}
onMounted(renderContent)
watch(() => renderFunction, renderContent)
watch(() => props.renderFunction, renderContent)
</script>

View File

@@ -52,7 +52,7 @@ import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
const { t } = useI18n()
const { modelValue, initialIcon, initialColor } = defineProps<{
const props = defineProps<{
modelValue: boolean
initialIcon?: string
initialColor?: string
@@ -64,7 +64,7 @@ const emit = defineEmits<{
}>()
const visible = computed({
get: () => modelValue,
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
@@ -96,13 +96,17 @@ const defaultIcon = iconOptions.find(
// @ts-expect-error fixme ts strict error
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
const finalColor = ref(initialColor || nodeBookmarkStore.defaultBookmarkColor)
const finalColor = ref(
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
)
const resetCustomization = () => {
// @ts-expect-error fixme ts strict error
selectedIcon.value =
iconOptions.find((option) => option.value === initialIcon) || defaultIcon
finalColor.value = initialColor || nodeBookmarkStore.defaultBookmarkColor
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
finalColor.value =
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
}
const confirmCustomization = () => {
@@ -115,7 +119,7 @@ const closeDialog = () => {
}
watch(
() => modelValue,
() => props.modelValue,
(newValue: boolean) => {
if (newValue) {
resetCustomization()

View File

@@ -5,7 +5,7 @@
{{ col.header }}
</div>
<div>
{{ formatValue(device[col.field], col.field) }}
{{ formatValue(props.device[col.field], col.field) }}
</div>
</template>
</div>
@@ -15,7 +15,7 @@
import type { DeviceStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
const { device } = defineProps<{
const props = defineProps<{
device: DeviceStats
}>()

View File

@@ -6,7 +6,7 @@ import { createApp } from 'vue'
import EditableText from './EditableText.vue'
describe(EditableText.__name ?? 'EditableText', () => {
describe('EditableText', () => {
beforeAll(() => {
// Create a Vue app instance for PrimeVue
const app = createApp({})

View File

@@ -5,10 +5,10 @@
<i v-if="status === 'completed'" class="pi pi-check text-green-500" />
<div class="file-info">
<div class="file-details">
<span class="file-type" :title="displayHint">{{ displayLabel }}</span>
<span class="file-type" :title="hint">{{ label }}</span>
</div>
<div v-if="error" class="file-error">
{{ error }}
<div v-if="props.error" class="file-error">
{{ props.error }}
</div>
</div>
@@ -18,14 +18,14 @@
class="file-action-button"
variant="secondary"
size="sm"
:disabled="!!error"
:disabled="!!props.error"
@click="triggerDownload"
>
<i class="pi pi-download" />
{{ $t('g.downloadWithSize', { size: fileSize }) }}
</Button>
<Button
v-if="(status === null || status === 'error') && !!url"
v-if="(status === null || status === 'error') && !!props.url"
variant="secondary"
size="sm"
@click="copyURL"
@@ -53,7 +53,7 @@
class="file-action-button"
variant="secondary"
size="sm"
:disabled="!!error"
:disabled="!!props.error"
@click="triggerPauseDownload"
>
<i class="pi pi-pause-circle" />
@@ -66,7 +66,7 @@
variant="secondary"
size="sm"
:aria-label="t('electronFileDownload.resume')"
:disabled="!!error"
:disabled="!!props.error"
@click="triggerResumeDownload"
>
<i class="pi pi-play-circle" />
@@ -78,7 +78,7 @@
variant="destructive"
size="sm"
:aria-label="t('electronFileDownload.cancel')"
:disabled="!!error"
:disabled="!!props.error"
@click="triggerCancelDownload"
>
<i class="pi pi-times-circle" />
@@ -98,7 +98,7 @@ import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import { formatSize } from '@/utils/formatUtil'
const { url, hint, label, error } = defineProps<{
const props = defineProps<{
url: string
hint?: string
label?: string
@@ -106,9 +106,9 @@ const { url, hint, label, error } = defineProps<{
}>()
const { t } = useI18n()
const displayLabel = computed(() => label || url.split('/').pop())
const displayHint = computed(() => hint || url)
const download = useDownload(url)
const label = computed(() => props.label || props.url.split('/').pop())
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const downloadProgress = ref<number>(0)
const status = ref<string | null>(null)
const fileSize = computed(() =>
@@ -117,10 +117,10 @@ const fileSize = computed(() =>
const { copyToClipboard } = useCopyToClipboard()
const electronDownloadStore = useElectronDownloadStore()
// @ts-expect-error fixme ts strict error
const [savePath, filename] = label.split('/')
const [savePath, filename] = props.label.split('/')
electronDownloadStore.$subscribe((_, { downloads }) => {
const download = downloads.find((download) => url === download.url)
const download = downloads.find((download) => props.url === download.url)
if (download) {
// @ts-expect-error fixme ts strict error
@@ -132,17 +132,17 @@ electronDownloadStore.$subscribe((_, { downloads }) => {
const triggerDownload = async () => {
await electronDownloadStore.start({
url,
url: props.url,
savePath: savePath.trim(),
filename: filename.trim()
})
}
const triggerCancelDownload = () => electronDownloadStore.cancel(url)
const triggerPauseDownload = () => electronDownloadStore.pause(url)
const triggerResumeDownload = () => electronDownloadStore.resume(url)
const triggerCancelDownload = () => electronDownloadStore.cancel(props.url)
const triggerPauseDownload = () => electronDownloadStore.pause(props.url)
const triggerResumeDownload = () => electronDownloadStore.resume(props.url)
const copyURL = async () => {
await copyToClipboard(url)
await copyToClipboard(props.url)
}
</script>

View File

@@ -5,7 +5,10 @@
:ref="
(el) => {
if (el)
mountCustomExtension(extension as CustomExtension, el as HTMLElement)
mountCustomExtension(
props.extension as CustomExtension,
el as HTMLElement
)
}
"
/>
@@ -16,17 +19,17 @@ import { onBeforeUnmount } from 'vue'
import type { CustomExtension, VueExtension } from '@/types/extensionTypes'
const { extension } = defineProps<{
const props = defineProps<{
extension: VueExtension | CustomExtension
}>()
const mountCustomExtension = (ext: CustomExtension, el: HTMLElement) => {
ext.render(el)
const mountCustomExtension = (extension: CustomExtension, el: HTMLElement) => {
extension.render(el)
}
onBeforeUnmount(() => {
if (extension.type === 'custom' && extension.destroy) {
extension.destroy()
if (props.extension.type === 'custom' && props.extension.destroy) {
props.extension.destroy()
}
})
</script>

View File

@@ -3,35 +3,35 @@
<div class="flex flex-row items-center gap-2">
<div>
<div>
<span :title="displayHint">{{ displayLabel }}</span>
<span :title="hint">{{ label }}</span>
</div>
<Message
v-if="error"
v-if="props.error"
severity="error"
icon="pi pi-exclamation-triangle"
size="small"
variant="outlined"
class="my-2 h-min max-w-xs px-1"
:title="error"
:title="props.error"
:pt="{
text: { class: 'overflow-hidden text-ellipsis' }
}"
>
{{ error }}
{{ props.error }}
</Message>
</div>
<div>
<Button
variant="secondary"
:disabled="!!error"
:title="url"
:disabled="!!props.error"
:title="props.url"
@click="download.triggerBrowserDownload"
>
{{ $t('g.downloadWithSize', { size: fileSize }) }}
</Button>
</div>
<div>
<Button variant="secondary" :disabled="!!error" @click="copyURL">
<Button variant="secondary" :disabled="!!props.error" @click="copyURL">
{{ $t('g.copyURL') }}
</Button>
</div>
@@ -47,22 +47,22 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'
const { url, hint, label, error } = defineProps<{
const props = defineProps<{
url: string
hint?: string
label?: string
error?: string
}>()
const displayLabel = computed(() => label || url.split('/').pop())
const label = computed(() => props.label || props.url.split('/').pop())
const displayHint = computed(() => hint || url)
const download = useDownload(url)
const hint = computed(() => props.hint || props.url)
const download = useDownload(props.url)
const fileSize = computed(() =>
download.fileSize.value ? formatSize(download.fileSize.value) : '?'
)
const copyURL = async () => {
await copyToClipboard(url)
await copyToClipboard(props.url)
}
const { copyToClipboard } = useCopyToClipboard()

View File

@@ -10,7 +10,7 @@ import ColorPicker from 'primevue/colorpicker'
import InputText from 'primevue/inputtext'
const modelValue = defineModel<string>('modelValue')
const { label } = defineProps<{
defineProps<{
label?: string
}>()

View File

@@ -45,7 +45,7 @@ import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
const { modelValue } = defineProps<{
defineProps<{
modelValue: string
}>()
@@ -64,9 +64,9 @@ const handleFileUpload = (event: Event) => {
if (target.files && target.files[0]) {
const file = target.files[0]
const reader = new FileReader()
reader.addEventListener('load', (e) => {
reader.onload = (e) => {
emit('update:modelValue', e.target?.result as string)
})
}
reader.readAsDataURL(file)
}
}

View File

@@ -2,12 +2,16 @@
<template>
<div class="flex flex-row items-center gap-2">
<div class="form-label flex grow items-center">
<span :id="`${id}-label`" class="text-muted" :class="labelClass">
<span
:id="`${props.id}-label`"
class="text-muted"
:class="props.labelClass"
>
<slot name="name-prefix" />
{{ item.name }}
{{ props.item.name }}
<i
v-if="item.tooltip"
v-tooltip="item.tooltip"
v-if="props.item.tooltip"
v-tooltip="props.item.tooltip"
class="pi pi-info-circle bg-transparent"
/>
<slot name="name-suffix" />
@@ -15,11 +19,11 @@
</div>
<div class="form-input flex justify-end">
<component
:is="markRaw(getFormComponent(item))"
:id="id"
:is="markRaw(getFormComponent(props.item))"
:id="props.id"
v-model:model-value="formValue"
:aria-labelledby="`${id}-label`"
v-bind="getFormAttrs(item)"
:aria-labelledby="`${props.id}-label`"
v-bind="getFormAttrs(props.item)"
/>
</div>
</div>
@@ -44,37 +48,35 @@ import UrlInput from '@/components/common/UrlInput.vue'
import type { FormItem } from '@/platform/settings/types'
const formValue = defineModel<unknown>('formValue')
const { item, id, labelClass } = defineProps<{
const props = defineProps<{
item: FormItem
id?: string
labelClass?: string | Record<string, boolean>
}>()
function getFormAttrs(formItem: FormItem) {
const attrs = { ...(formItem.attrs || {}) }
const inputType = formItem.type
function getFormAttrs(item: FormItem) {
const attrs = { ...(item.attrs || {}) }
const inputType = item.type
if (typeof inputType === 'function') {
attrs['renderFunction'] = () =>
inputType(
formItem.name,
(v: unknown) => {
formValue.value = v
},
props.item.name,
(v: unknown) => (formValue.value = v),
formValue.value,
formItem.attrs
item.attrs
)
}
switch (formItem.type) {
switch (item.type) {
case 'combo':
case 'radio':
attrs['options'] =
typeof formItem.options === 'function'
typeof item.options === 'function'
? // @ts-expect-error: Audit and deprecate usage of legacy options type:
// (value) => [string | {text: string, value: string}]
formItem.options(formValue.value)
: formItem.options
item.options(formValue.value)
: item.options
if (typeof formItem.options?.[0] !== 'string') {
if (typeof item.options?.[0] !== 'string') {
attrs['optionLabel'] = 'text'
attrs['optionValue'] = 'value'
}
@@ -83,11 +85,11 @@ function getFormAttrs(formItem: FormItem) {
return attrs
}
function getFormComponent(formItem: FormItem): Component {
if (typeof formItem.type === 'function') {
function getFormComponent(item: FormItem): Component {
if (typeof item.type === 'function') {
return CustomFormValue
}
switch (formItem.type) {
switch (item.type) {
case 'boolean':
return ToggleSwitch
case 'number':

View File

@@ -5,242 +5,239 @@ import { beforeAll, describe, expect, it } from 'vitest'
import { createApp } from 'vue'
import type { SettingOption } from '@/platform/settings/types'
import type { ComponentProps } from 'vue-component-type-helpers'
import FormRadioGroup from './FormRadioGroup.vue'
import type { ComponentProps } from 'vue-component-type-helpers'
describe(
(FormRadioGroup as { __name?: string }).__name ?? 'FormRadioGroup',
() => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
})
describe('FormRadioGroup', () => {
beforeAll(() => {
const app = createApp({})
app.use(PrimeVue)
})
type FormRadioGroupProps = ComponentProps<typeof FormRadioGroup>
const mountComponent = (props: FormRadioGroupProps, options = {}) => {
return mount(FormRadioGroup, {
global: {
plugins: [PrimeVue],
components: { RadioButton }
},
props,
...options
})
}
describe('normalizedOptions computed property', () => {
it('handles string array options', () => {
const wrapper = mountComponent({
modelValue: 'option1',
options: ['option1', 'option2', 'option3'],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('option1')
expect(radioButtons[1].props('value')).toBe('option2')
expect(radioButtons[2].props('value')).toBe('option3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('option1')
expect(labels[1].text()).toBe('option2')
expect(labels[2].text()).toBe('option3')
})
it('handles SettingOption array', () => {
const options: SettingOption[] = [
{ text: 'Small', value: 'sm' },
{ text: 'Medium', value: 'md' },
{ text: 'Large', value: 'lg' }
]
const wrapper = mountComponent({
modelValue: 'md',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('sm')
expect(radioButtons[1].props('value')).toBe('md')
expect(radioButtons[2].props('value')).toBe('lg')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Small')
expect(labels[1].text()).toBe('Medium')
expect(labels[2].text()).toBe('Large')
})
it('handles SettingOption with undefined value (uses text as value)', () => {
const options: SettingOption[] = [
{ text: 'Option A', value: undefined },
{ text: 'Option B' }
]
const wrapper = mountComponent({
modelValue: 'Option A',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('value')).toBe('Option A')
expect(radioButtons[1].props('value')).toBe('Option B')
})
it('handles custom object with optionLabel and optionValue', () => {
const options = [
{ name: 'First Option', id: '1' },
{ name: 'Second Option', id: '2' },
{ name: 'Third Option', id: '3' }
]
const wrapper = mountComponent({
modelValue: 2,
options,
optionLabel: 'name',
optionValue: 'id',
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('1')
expect(radioButtons[1].props('value')).toBe('2')
expect(radioButtons[2].props('value')).toBe('3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('First Option')
expect(labels[1].text()).toBe('Second Option')
expect(labels[2].text()).toBe('Third Option')
})
it('handles mixed array with strings and SettingOptions', () => {
const options: (string | SettingOption)[] = [
'Simple String',
{ text: 'Complex Option', value: 'complex' },
'Another String'
]
const wrapper = mountComponent({
modelValue: 'complex',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('Simple String')
expect(radioButtons[1].props('value')).toBe('complex')
expect(radioButtons[2].props('value')).toBe('Another String')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Simple String')
expect(labels[1].text()).toBe('Complex Option')
expect(labels[2].text()).toBe('Another String')
})
it('handles empty options array', () => {
const wrapper = mountComponent({
modelValue: null,
options: [],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles undefined options gracefully', () => {
const wrapper = mountComponent({
modelValue: null,
options: undefined,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles object with missing properties gracefully', () => {
const options = [{ label: 'Option 1', val: 'opt1' }]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(1)
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Unknown')
})
})
describe('component functionality', () => {
it('sets correct input-id and name attributes', () => {
const options = ['A', 'B']
const wrapper = mountComponent({
modelValue: 'A',
options,
id: 'my-radio-group'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('inputId')).toBe('my-radio-group-A')
expect(radioButtons[0].props('name')).toBe('my-radio-group')
expect(radioButtons[1].props('inputId')).toBe('my-radio-group-B')
expect(radioButtons[1].props('name')).toBe('my-radio-group')
})
it('associates labels with radio buttons correctly', () => {
const options = ['Yes', 'No']
const wrapper = mountComponent({
modelValue: 'Yes',
options,
id: 'confirm-radio'
})
const labels = wrapper.findAll('label')
expect(labels[0].attributes('for')).toBe('confirm-radio-Yes')
expect(labels[1].attributes('for')).toBe('confirm-radio-No')
})
it('sets aria-describedby attribute correctly', () => {
const options: SettingOption[] = [
{ text: 'Option 1', value: 'opt1' },
{ text: 'Option 2', value: 'opt2' }
]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].attributes('aria-describedby')).toBe(
'Option 1-label'
)
expect(radioButtons[1].attributes('aria-describedby')).toBe(
'Option 2-label'
)
})
type FormRadioGroupProps = ComponentProps<typeof FormRadioGroup>
const mountComponent = (props: FormRadioGroupProps, options = {}) => {
return mount(FormRadioGroup, {
global: {
plugins: [PrimeVue],
components: { RadioButton }
},
props,
...options
})
}
)
describe('normalizedOptions computed property', () => {
it('handles string array options', () => {
const wrapper = mountComponent({
modelValue: 'option1',
options: ['option1', 'option2', 'option3'],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('option1')
expect(radioButtons[1].props('value')).toBe('option2')
expect(radioButtons[2].props('value')).toBe('option3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('option1')
expect(labels[1].text()).toBe('option2')
expect(labels[2].text()).toBe('option3')
})
it('handles SettingOption array', () => {
const options: SettingOption[] = [
{ text: 'Small', value: 'sm' },
{ text: 'Medium', value: 'md' },
{ text: 'Large', value: 'lg' }
]
const wrapper = mountComponent({
modelValue: 'md',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('sm')
expect(radioButtons[1].props('value')).toBe('md')
expect(radioButtons[2].props('value')).toBe('lg')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Small')
expect(labels[1].text()).toBe('Medium')
expect(labels[2].text()).toBe('Large')
})
it('handles SettingOption with undefined value (uses text as value)', () => {
const options: SettingOption[] = [
{ text: 'Option A', value: undefined },
{ text: 'Option B' }
]
const wrapper = mountComponent({
modelValue: 'Option A',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('value')).toBe('Option A')
expect(radioButtons[1].props('value')).toBe('Option B')
})
it('handles custom object with optionLabel and optionValue', () => {
const options = [
{ name: 'First Option', id: '1' },
{ name: 'Second Option', id: '2' },
{ name: 'Third Option', id: '3' }
]
const wrapper = mountComponent({
modelValue: 2,
options,
optionLabel: 'name',
optionValue: 'id',
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('1')
expect(radioButtons[1].props('value')).toBe('2')
expect(radioButtons[2].props('value')).toBe('3')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('First Option')
expect(labels[1].text()).toBe('Second Option')
expect(labels[2].text()).toBe('Third Option')
})
it('handles mixed array with strings and SettingOptions', () => {
const options: (string | SettingOption)[] = [
'Simple String',
{ text: 'Complex Option', value: 'complex' },
'Another String'
]
const wrapper = mountComponent({
modelValue: 'complex',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(3)
expect(radioButtons[0].props('value')).toBe('Simple String')
expect(radioButtons[1].props('value')).toBe('complex')
expect(radioButtons[2].props('value')).toBe('Another String')
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Simple String')
expect(labels[1].text()).toBe('Complex Option')
expect(labels[2].text()).toBe('Another String')
})
it('handles empty options array', () => {
const wrapper = mountComponent({
modelValue: null,
options: [],
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles undefined options gracefully', () => {
const wrapper = mountComponent({
modelValue: null,
options: undefined,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(0)
})
it('handles object with missing properties gracefully', () => {
const options = [{ label: 'Option 1', val: 'opt1' }]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons).toHaveLength(1)
const labels = wrapper.findAll('label')
expect(labels[0].text()).toBe('Unknown')
})
})
describe('component functionality', () => {
it('sets correct input-id and name attributes', () => {
const options = ['A', 'B']
const wrapper = mountComponent({
modelValue: 'A',
options,
id: 'my-radio-group'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].props('inputId')).toBe('my-radio-group-A')
expect(radioButtons[0].props('name')).toBe('my-radio-group')
expect(radioButtons[1].props('inputId')).toBe('my-radio-group-B')
expect(radioButtons[1].props('name')).toBe('my-radio-group')
})
it('associates labels with radio buttons correctly', () => {
const options = ['Yes', 'No']
const wrapper = mountComponent({
modelValue: 'Yes',
options,
id: 'confirm-radio'
})
const labels = wrapper.findAll('label')
expect(labels[0].attributes('for')).toBe('confirm-radio-Yes')
expect(labels[1].attributes('for')).toBe('confirm-radio-No')
})
it('sets aria-describedby attribute correctly', () => {
const options: SettingOption[] = [
{ text: 'Option 1', value: 'opt1' },
{ text: 'Option 2', value: 'opt2' }
]
const wrapper = mountComponent({
modelValue: 'opt1',
options,
id: 'test-radio'
})
const radioButtons = wrapper.findAllComponents(RadioButton)
expect(radioButtons[0].attributes('aria-describedby')).toBe(
'Option 1-label'
)
expect(radioButtons[1].attributes('aria-describedby')).toBe(
'Option 2-label'
)
})
})
})

View File

@@ -26,7 +26,7 @@ import { computed } from 'vue'
import type { SettingOption } from '@/platform/settings/types'
const { modelValue, options, optionLabel, optionValue, id } = defineProps<{
const props = defineProps<{
modelValue: T
options?: (string | SettingOption | Record<string, string>)[]
optionLabel?: string
@@ -39,9 +39,9 @@ defineEmits<{
}>()
const normalizedOptions = computed<SettingOption[]>(() => {
if (!options) return []
if (!props.options) return []
return options.map((option) => {
return props.options.map((option) => {
if (typeof option === 'string') {
return { text: option, value: option }
}
@@ -54,8 +54,8 @@ const normalizedOptions = computed<SettingOption[]>(() => {
}
// Handle optionLabel/optionValue
return {
text: option[optionLabel || 'text'] || 'Unknown',
value: option[optionValue || 'value']
text: option[props.optionLabel || 'text'] || 'Unknown',
value: option[props.optionValue || 'value']
}
})
})

View File

@@ -30,25 +30,24 @@ import InputNumber from 'primevue/inputnumber'
import Knob from 'primevue/knob'
import { ref, watch } from 'vue'
const { modelValue, inputClass, knobClass, min, max, step, resolution } =
defineProps<{
modelValue: number
inputClass?: string
knobClass?: string
min?: number
max?: number
step?: number
resolution?: number
}>()
const props = defineProps<{
modelValue: number
inputClass?: string
knobClass?: string
min?: number
max?: number
step?: number
resolution?: number
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>()
const localValue = ref(modelValue)
const localValue = ref(props.modelValue)
watch(
() => modelValue,
() => props.modelValue,
(newValue) => {
localValue.value = newValue
}
@@ -57,18 +56,18 @@ watch(
const updateValue = (newValue: number | null) => {
if (newValue === null) {
// If the input is cleared, reset to the minimum value or 0
newValue = Number(min) || 0
newValue = Number(props.min) || 0
}
const minVal = Number(min ?? Number.NEGATIVE_INFINITY)
const maxVal = Number(max ?? Number.POSITIVE_INFINITY)
const stepVal = Number(step) || 1
const min = Number(props.min ?? Number.NEGATIVE_INFINITY)
const max = Number(props.max ?? Number.POSITIVE_INFINITY)
const step = Number(props.step) || 1
// Ensure the value is within the allowed range
newValue = Math.max(minVal, Math.min(maxVal, newValue))
newValue = Math.max(min, Math.min(max, newValue))
// Round to the nearest step
newValue = Math.round(newValue / stepVal) * stepVal
newValue = Math.round(newValue / step) * step
// Update local value and emit change
localValue.value = newValue
@@ -77,11 +76,11 @@ const updateValue = (newValue: number | null) => {
const displayValue = (value: number): string => {
updateValue(value)
const stepString = (step ?? 1).toString()
const decimalPlaces = stepString.includes('.')
const stepString = (props.step ?? 1).toString()
const resolution = stepString.includes('.')
? stepString.split('.')[1].length
: 0
return value.toFixed(resolution ?? decimalPlaces)
return value.toFixed(props.resolution ?? resolution)
}
defineOptions({

View File

@@ -29,7 +29,7 @@ import InputNumber from 'primevue/inputnumber'
import Slider from 'primevue/slider'
import { ref, watch } from 'vue'
const { modelValue, inputClass, sliderClass, min, max, step } = defineProps<{
const props = defineProps<{
modelValue: number
inputClass?: string
sliderClass?: string
@@ -42,10 +42,10 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>()
const localValue = ref(modelValue)
const localValue = ref(props.modelValue)
watch(
() => modelValue,
() => props.modelValue,
(newValue) => {
localValue.value = newValue
}
@@ -54,18 +54,18 @@ watch(
const updateValue = (newValue: number | null) => {
if (newValue === null) {
// If the input is cleared, reset to the minimum value or 0
newValue = Number(min) || 0
newValue = Number(props.min) || 0
}
const minVal = Number(min ?? Number.NEGATIVE_INFINITY)
const maxVal = Number(max ?? Number.POSITIVE_INFINITY)
const stepVal = Number(step) || 1
const min = Number(props.min ?? Number.NEGATIVE_INFINITY)
const max = Number(props.max ?? Number.POSITIVE_INFINITY)
const step = Number(props.step) || 1
// Ensure the value is within the allowed range
newValue = Math.max(minVal, Math.min(maxVal, newValue))
newValue = Math.max(min, Math.min(max, newValue))
// Round to the nearest step
newValue = Math.round(newValue / stepVal) * stepVal
newValue = Math.round(newValue / step) * step
// Update local value and emit change
localValue.value = newValue

View File

@@ -41,6 +41,7 @@ const spinnerSizeClass = computed(() => {
switch (size) {
case 'sm':
return 'h-6 w-6 border-2'
case 'md':
default:
return 'h-12 w-12 border-4'
}

View File

@@ -1,5 +1,5 @@
<template>
<div :class="cn('no-results-placeholder h-full p-8', className)">
<div class="no-results-placeholder h-full p-8" :class="props.class">
<Card>
<template #content>
<div class="flex flex-col items-center">
@@ -25,16 +25,8 @@
import Card from 'primevue/card'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
const {
class: className,
icon,
title,
message,
textClass,
buttonLabel
} = defineProps<{
const props = defineProps<{
class?: string
icon?: string
title: string

View File

@@ -19,7 +19,7 @@ const i18n = createI18n({
}
})
describe((SearchBox as { __name?: string }).__name ?? 'SearchBox', () => {
describe('SearchBox', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()

View File

@@ -18,7 +18,7 @@ export interface SearchFilter {
id: string | number
}
const { text, badge, badgeClass } = defineProps<Omit<SearchFilter, 'id'>>()
defineProps<Omit<SearchFilter, 'id'>>()
defineEmits(['remove'])
</script>

View File

@@ -21,9 +21,9 @@
<h2 class="mb-4 text-2xl font-semibold">
{{ $t('g.devices') }}
</h2>
<TabView v-if="stats.devices.length > 1">
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in stats.devices"
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
@@ -31,7 +31,7 @@
<DeviceInfo :device="device" />
</TabPanel>
</TabView>
<DeviceInfo v-else :device="stats.devices[0]" />
<DeviceInfo v-else :device="props.stats.devices[0]" />
</div>
</template>
</div>
@@ -48,16 +48,16 @@ import { isCloud } from '@/platform/distribution/types'
import type { SystemStats } from '@/schemas/apiSchema'
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
const { stats } = defineProps<{
const props = defineProps<{
stats: SystemStats
}>()
const systemInfo = computed(() => ({
...stats.system,
argv: stats.system.argv.join(' ')
...props.stats.system,
argv: props.stats.system.argv.join(' ')
}))
const hasDevices = computed(() => stats.devices.length > 0)
const hasDevices = computed(() => props.stats.devices.length > 0)
type SystemInfoKey = keyof SystemStats['system']

View File

@@ -4,7 +4,7 @@
v-model:expanded-keys="expandedKeys"
v-model:selection-keys="selectionKeys"
class="tree-explorer px-2 py-0 2xl:px-4 bg-transparent"
:class="className"
:class="props.class"
:value="renderedRoot.children"
selection-mode="single"
:pt="{
@@ -37,6 +37,10 @@
<ContextMenu ref="menu" :model="menuItems" />
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem, MenuItemCommandEvent } from 'primevue/menuitem'
import Tree from 'primevue/tree'
@@ -56,10 +60,6 @@ import type {
} from '@/types/treeExplorerTypes'
import { combineTrees, findNodeByKey } from '@/utils/treeUtil'
defineOptions({
inheritAttrs: false
})
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys', {
required: true
})
@@ -68,7 +68,7 @@ const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
// Tracks whether the caller has set the selectionKeys model.
const storeSelectionKeys = selectionKeys.value !== undefined
const { root, class: className } = defineProps<{
const props = defineProps<{
root: TreeExplorerNode
class?: string
}>()
@@ -90,7 +90,7 @@ const {
)
const renderedRoot = computed<RenderedTreeExplorerNode>(() => {
const renderedRoot = fillNodeInfo(root)
const renderedRoot = fillNodeInfo(props.root)
return newFolderNode.value
? combineTrees(renderedRoot, newFolderNode.value)
: renderedRoot

View File

@@ -19,7 +19,7 @@ const i18n = createI18n({
messages: {}
})
describe(TreeExplorerTreeNode.__name ?? 'TreeExplorerTreeNode', () => {
describe('TreeExplorerTreeNode', () => {
const mockNode = {
key: '1',
label: 'Test Node',

View File

@@ -5,21 +5,21 @@
'tree-node',
{
'can-drop': canDrop,
'tree-folder': !node.leaf,
'tree-leaf': node.leaf
'tree-folder': !props.node.leaf,
'tree-leaf': props.node.leaf
}
]"
:data-testid="`tree-node-${node.key}`"
>
<div class="node-content">
<span class="node-label">
<slot name="before-label" :node="node" />
<slot name="before-label" :node="props.node" />
<EditableText
:model-value="node.label"
:is-editing="isEditing"
@edit="handleRename"
/>
<slot name="after-label" :node="node" />
<slot name="after-label" :node="props.node" />
</span>
<Badge
v-if="showNodeBadgeText"
@@ -31,7 +31,7 @@
<div
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
>
<slot name="actions" :node="node" />
<slot name="actions" :node="props.node" />
</div>
</div>
</template>
@@ -52,7 +52,7 @@ import type {
TreeExplorerDragAndDropData
} from '@/types/treeExplorerTypes'
const { node } = defineProps<{
const props = defineProps<{
node: RenderedTreeExplorerNode
}>()
@@ -67,20 +67,20 @@ const emit = defineEmits<{
}>()
const nodeBadgeText = computed<string>(() => {
if (node.leaf) {
if (props.node.leaf) {
return ''
}
if (node.badgeText !== undefined && node.badgeText !== null) {
return node.badgeText
if (props.node.badgeText !== undefined && props.node.badgeText !== null) {
return props.node.badgeText
}
return node.totalLeaves.toString()
return props.node.totalLeaves.toString()
})
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
const isEditing = computed<boolean>(() => node.isEditingLabel ?? false)
const isEditing = computed<boolean>(() => props.node.isEditingLabel ?? false)
const handleEditLabel = inject(InjectKeyHandleEditLabelFunction)
const handleRename = (newName: string) => {
handleEditLabel?.(node, newName)
handleEditLabel?.(props.node, newName)
}
const container = ref<HTMLElement | null>(null)
@@ -89,21 +89,21 @@ const canDrop = ref(false)
const treeNodeElementGetter = () =>
container.value?.closest('.p-tree-node-content') as HTMLElement
if (node.draggable) {
if (props.node.draggable) {
usePragmaticDraggable(treeNodeElementGetter, {
getInitialData: () => {
return {
type: 'tree-explorer-node',
data: node
data: props.node
}
},
onDragStart: () => emit('dragStart', node),
onDrop: () => emit('dragEnd', node),
onGenerateDragPreview: node.renderDragPreview
onDragStart: () => emit('dragStart', props.node),
onDrop: () => emit('dragEnd', props.node),
onGenerateDragPreview: props.node.renderDragPreview
? ({ nativeSetDragImage }) => {
setCustomNativeDragPreview({
render: ({ container }) => {
return node.renderDragPreview?.(container)
return props.node.renderDragPreview?.(container)
},
nativeSetDragImage
})
@@ -112,14 +112,14 @@ if (node.draggable) {
})
}
if (node.droppable) {
if (props.node.droppable) {
usePragmaticDroppable(treeNodeElementGetter, {
onDrop: async (event) => {
const dndData = event.source.data as TreeExplorerDragAndDropData
if (dndData.type === 'tree-explorer-node') {
await node.handleDrop?.(dndData)
await props.node.handleDrop?.(dndData)
canDrop.value = false
emit('itemDropped', node, dndData.data)
emit('itemDropped', props.node, dndData.data)
}
},
onDragEnter: (event) => {

View File

@@ -6,11 +6,10 @@ import InputText from 'primevue/inputtext'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import UrlInput from './UrlInput.vue'
import type { ComponentProps } from 'vue-component-type-helpers'
import UrlInput from './UrlInput.vue'
describe(UrlInput.__name ?? 'UrlInput', () => {
describe('UrlInput', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)

View File

@@ -17,7 +17,7 @@
'pi pi-times cursor-pointer text-red-500':
validationState === ValidationState.INVALID
}"
@click="validateUrl(model)"
@click="validateUrl(props.modelValue)"
/>
</IconField>
</template>
@@ -32,34 +32,40 @@ import { isValidUrl } from '@/utils/formatUtil'
import { checkUrlReachable } from '@/utils/networkUtil'
import { ValidationState } from '@/utils/validationUtil'
const model = defineModel<string>({ required: true })
const { validateUrlFn } = defineProps<{
const props = defineProps<{
modelValue: string
validateUrlFn?: (url: string) => Promise<boolean>
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
'state-change': [state: ValidationState]
}>()
const validationState = ref<ValidationState>(ValidationState.IDLE)
const cleanInput = (value: string): string =>
value ? value.replaceAll(/\s+/g, '') : ''
value ? value.replace(/\s+/g, '') : ''
const internalValue = ref(cleanInput(model.value))
// Add internal value state
const internalValue = ref(cleanInput(props.modelValue))
watch(model, async (newValue: string) => {
internalValue.value = cleanInput(newValue)
await validateUrl(newValue)
})
// Watch for external modelValue changes
watch(
() => props.modelValue,
async (newValue: string) => {
internalValue.value = cleanInput(newValue)
await validateUrl(newValue)
}
)
watch(validationState, (newState) => {
emit('state-change', newState)
})
// Validate on mount
onMounted(async () => {
await validateUrl(model.value)
await validateUrl(props.modelValue)
})
const handleInput = (value: string | undefined) => {
@@ -81,7 +87,7 @@ const handleBlur = async () => {
}
// Emit the update only on blur
model.value = normalizedUrl
emit('update:modelValue', normalizedUrl)
}
// Default validation implementation
@@ -107,7 +113,7 @@ const validateUrl = async (value: string) => {
validationState.value = ValidationState.LOADING
try {
const isValid = await (validateUrlFn ?? defaultValidateUrl)(url)
const isValid = await (props.validateUrlFn ?? defaultValidateUrl)(url)
validationState.value = isValid
? ValidationState.VALID
: ValidationState.INVALID

View File

@@ -23,7 +23,7 @@ const i18n = createI18n({
}
})
describe(UserAvatar.__name ?? 'UserAvatar', () => {
describe('UserAvatar', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)

View File

@@ -39,7 +39,7 @@ vi.mock('@/stores/firebaseAuthStore', () => ({
}))
}))
describe(UserCredit.__name ?? 'UserCredit', () => {
describe('UserCredit', () => {
beforeEach(() => {
vi.clearAllMocks()
mockBalance.value = {

View File

@@ -444,6 +444,7 @@ const distributions = computed(() => {
return [TemplateIncludeOnDistributionEnum.Cloud]
case 'localhost':
return [TemplateIncludeOnDistributionEnum.Local]
case 'desktop':
default:
if (systemStatsStore.systemStats?.system.os === 'darwin') {
return [
@@ -594,10 +595,12 @@ const coordinateNavAndSort = (source: 'nav' | 'sort') => {
// When navigating away from 'Popular' category while sort is 'Popular', reset sort to default.
sortBy.value = 'default'
}
} else if (source === 'sort' && isPopularNav && !isPopularSort) {
} else if (source === 'sort') {
// When sort is changed away from 'Popular' while in the 'Popular' category,
// reset the category to 'All Templates' to avoid a confusing state.
selectedNavItem.value = 'all'
if (isPopularNav && !isPopularSort) {
selectedNavItem.value = 'all'
}
}
}
@@ -678,37 +681,37 @@ const runsOnOptions = computed(() =>
const modelFilterLabel = computed(() => {
if (selectedModelObjects.value.length === 0) {
return t('templateWorkflows.modelFilter', 'Model Filter')
}
if (selectedModelObjects.value.length === 1) {
} else if (selectedModelObjects.value.length === 1) {
return selectedModelObjects.value[0].name
} else {
return t('templateWorkflows.modelsSelected', {
count: selectedModelObjects.value.length
})
}
return t('templateWorkflows.modelsSelected', {
count: selectedModelObjects.value.length
})
})
const useCaseFilterLabel = computed(() => {
if (selectedUseCaseObjects.value.length === 0) {
return t('templateWorkflows.useCaseFilter', 'Use Case')
}
if (selectedUseCaseObjects.value.length === 1) {
} else if (selectedUseCaseObjects.value.length === 1) {
return selectedUseCaseObjects.value[0].name
} else {
return t('templateWorkflows.useCasesSelected', {
count: selectedUseCaseObjects.value.length
})
}
return t('templateWorkflows.useCasesSelected', {
count: selectedUseCaseObjects.value.length
})
})
const runsOnFilterLabel = computed(() => {
if (selectedRunsOnObjects.value.length === 0) {
return t('templateWorkflows.runsOnFilter', 'Runs On')
}
if (selectedRunsOnObjects.value.length === 1) {
} else if (selectedRunsOnObjects.value.length === 1) {
return selectedRunsOnObjects.value[0].name
} else {
return t('templateWorkflows.runsOnSelected', {
count: selectedRunsOnObjects.value.length
})
}
return t('templateWorkflows.runsOnSelected', {
count: selectedRunsOnObjects.value.length
})
})
// Sort options

View File

@@ -26,7 +26,7 @@ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault()
return true
}
return
return undefined
}
onMounted(() => {

View File

@@ -6,7 +6,7 @@
</div>
</template>
<script setup lang="ts">
const { title } = defineProps<{
defineProps<{
title?: string
}>()
</script>

View File

@@ -120,13 +120,7 @@ import { useDialogService } from '@/services/dialogService'
import type { ConfirmationDialogType } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
const {
message,
type,
onConfirm: onConfirmProp,
itemList,
hint
} = defineProps<{
const props = defineProps<{
message: string
type: ConfirmationDialogType
onConfirm: (value?: boolean) => void
@@ -149,14 +143,14 @@ function openBlueprintOverwriteSetting() {
const doNotAskAgain = ref(false)
const onDeny = () => {
onConfirmProp(false)
props.onConfirm(false)
useDialogStore().closeDialog()
}
const onConfirm = () => {
if (type === 'overwriteBlueprint' && doNotAskAgain.value)
if (props.type === 'overwriteBlueprint' && doNotAskAgain.value)
void useSettingStore().set('Comfy.Workflow.WarnBlueprintOverwrite', false)
onConfirmProp(true)
props.onConfirm(true)
useDialogStore().closeDialog()
}
</script>

View File

@@ -30,7 +30,7 @@ const createMockNode = (type: string, version?: string): LGraphNode =>
outputs: []
})
describe(MissingCoreNodesMessage.__name ?? 'MissingCoreNodesMessage', () => {
describe('MissingCoreNodesMessage', () => {
const mockSystemStatsStore = {
systemStats: null as { system?: { comfyui_version?: string } } | null,
refetchSystemStats: vi.fn()

View File

@@ -94,7 +94,7 @@ interface ModelInfo {
folder_path?: string
}
const { missingModels: missingModelsProp, paths } = defineProps<{
const props = defineProps<{
missingModels: ModelInfo[]
paths: Record<string, string[]>
}>()
@@ -113,9 +113,9 @@ function openShowMissingModelsSetting() {
const modelDownloads = ref<Record<string, ModelInfo>>({})
const missingModels = computed(() => {
return missingModelsProp.map((model) => {
const modelPaths = paths[model.directory]
if (model.directory_invalid || !modelPaths) {
return props.missingModels.map((model) => {
const paths = props.paths[model.directory]
if (model.directory_invalid || !paths) {
return {
label: `${model.directory} / ${model.name}`,
url: model.url,
@@ -130,7 +130,7 @@ const missingModels = computed(() => {
name: model.name,
directory: model.directory,
url: model.url,
folder_path: modelPaths[0]
folder_path: paths[0]
}
modelDownloads.value[model.name] = downloadInfo
if (!whiteListedUrls.has(model.url)) {
@@ -157,7 +157,7 @@ const missingModels = computed(() => {
progress: downloadInfo.progress,
error: downloadInfo.error,
name: model.name,
paths: modelPaths,
paths: paths,
folderPath: downloadInfo.folder_path
}
})

View File

@@ -54,7 +54,7 @@ import { isCloud } from '@/platform/distribution/types'
import type { MissingNodeType } from '@/types/comfy'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
const { missingNodeTypes } = defineProps<{
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
@@ -63,7 +63,7 @@ const { missingCoreNodes } = useMissingNodes()
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return missingNodeTypes
return props.missingNodeTypes
.filter((node) => {
const type = typeof node === 'object' ? node.type : node
if (seenTypes.has(type)) return false

View File

@@ -25,22 +25,17 @@ import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useDialogStore } from '@/stores/dialogStore'
const {
message,
defaultValue,
onConfirm: onConfirmProp,
placeholder
} = defineProps<{
const props = defineProps<{
message: string
defaultValue: string
onConfirm: (value: string) => void
placeholder?: string
}>()
const inputValue = ref<string>(defaultValue)
const inputValue = ref<string>(props.defaultValue)
const onConfirm = () => {
onConfirmProp(inputValue.value)
props.onConfirm(inputValue.value)
useDialogStore().closeDialog()
}

View File

@@ -25,7 +25,7 @@ const mountOption = (
}
})
describe(CreditTopUpOption.__name ?? 'CreditTopUpOption', () => {
describe('CreditTopUpOption', () => {
it('renders credit amount and description', () => {
const wrapper = mountOption({ credits: 5000, description: '~500 videos*' })
expect(wrapper.text()).toContain('5,000')

View File

@@ -11,20 +11,23 @@ import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useTelemetry } from '@/platform/telemetry'
const { errorMessage, repoOwner, repoName } = defineProps<{
const props = defineProps<{
errorMessage: string
repoOwner: string
repoName: string
}>()
const queryString = computed(() => `${errorMessage} is:issue`)
const queryString = computed(() => props.errorMessage + ' is:issue')
/**
* Open GitHub issues search and track telemetry.
*/
const openGitHubIssues = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'error_dialog_find_existing_issues_clicked'
})
const query = encodeURIComponent(queryString.value)
const url = `https://github.com/${repoOwner}/${repoName}/issues?q=${query}`
const url = `https://github.com/${props.repoOwner}/${props.repoName}/issues?q=${query}`
window.open(url, '_blank')
}
</script>

View File

@@ -268,7 +268,7 @@ async function saveKeybinding() {
const commandId = currentEditingCommand.value?.id
const combo = newBindingKeyCombo.value
cancelEdit()
if (!combo || commandId == null) return
if (!combo || commandId == undefined) return
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({ commandId, combo })

View File

@@ -1,5 +1,5 @@
<template>
<TabPanel :value class="h-full w-full" :class="panelClass">
<TabPanel :value="props.value" class="h-full w-full" :class="props.class">
<div class="flex h-full w-full flex-col gap-2">
<slot name="header" />
<ScrollPanel class="h-0 grow pr-2">
@@ -14,7 +14,7 @@
import ScrollPanel from 'primevue/scrollpanel'
import TabPanel from 'primevue/tabpanel'
const { value, class: panelClass } = defineProps<{
const props = defineProps<{
value: string
class?: string
}>()

View File

@@ -17,7 +17,7 @@ vi.mock('@/utils/formatUtil', () => ({
normalizeI18nKey: vi.fn()
}))
describe(SettingItem.__name ?? 'SettingItem', () => {
describe('SettingItem', () => {
const mountComponent = (props: Record<string, unknown>, options = {}) => {
return mount(SettingItem, {
global: {

View File

@@ -76,7 +76,7 @@ const i18n = createI18n({
}
})
describe(UsageLogsTable.__name ?? 'UsageLogsTable', () => {
describe('UsageLogsTable', () => {
const mockEventsResponse = {
events: [
{

View File

@@ -169,10 +169,9 @@ const loadEvents = async () => {
} else {
error.value = customerEventService.error.value || 'Failed to load events'
}
} catch (errorCaught) {
error.value =
errorCaught instanceof Error ? errorCaught.message : 'Unknown error'
console.error('Error loading events:', errorCaught)
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
console.error('Error loading events:', err)
} finally {
loading.value = false
}

View File

@@ -56,7 +56,7 @@ const i18n = createI18n({
}
})
describe(ApiKeyForm.__name ?? 'ApiKeyForm', () => {
describe('ApiKeyForm', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)

View File

@@ -58,7 +58,7 @@ vi.mock('primevue/usetoast', () => ({
}))
}))
describe(SignInForm.__name ?? 'SignInForm', () => {
describe('SignInForm', () => {
beforeEach(() => {
vi.clearAllMocks()
mockSendPasswordReset.mockReset()
@@ -110,17 +110,12 @@ describe(SignInForm.__name ?? 'SignInForm', () => {
'span.text-muted.text-base.font-medium.cursor-pointer'
)
// Mock querySelector to track focus on the email input
// Mock getElementById to track focus
const mockFocus = vi.fn()
const mockElement: Partial<HTMLElement> = { focus: mockFocus }
const originalQuerySelector = document.querySelector.bind(document)
const spy = vi
.spyOn(document, 'querySelector')
.mockImplementation((selector: string) => {
if (selector === '#comfy-org-sign-in-email')
return mockElement as HTMLElement
return originalQuerySelector(selector)
})
vi.spyOn(document, 'getElementById').mockReturnValue(
mockElement as HTMLElement
)
// Click forgot password link while email is empty
await forgotPasswordSpan.trigger('click')
@@ -134,9 +129,10 @@ describe(SignInForm.__name ?? 'SignInForm', () => {
})
// Should focus email input
expect(spy).toHaveBeenCalledWith('#comfy-org-sign-in-email')
expect(document.getElementById).toHaveBeenCalledWith(
'comfy-org-sign-in-email'
)
expect(mockFocus).toHaveBeenCalled()
spy.mockRestore()
// Should NOT call sendPasswordReset
expect(mockSendPasswordReset).not.toHaveBeenCalled()
@@ -216,7 +212,7 @@ describe(SignInForm.__name ?? 'SignInForm', () => {
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(true)
expect(wrapper.findComponent(Button).exists()).toBe(false)
} catch {
} catch (error) {
// Fallback test - check HTML content if component rendering fails
mockLoading = true
const wrapper = mountComponent()
@@ -274,25 +270,21 @@ describe(SignInForm.__name ?? 'SignInForm', () => {
onSubmit: (data: { valid: boolean; values: unknown }) => void
}
// Mock querySelector to track focus on the email input
// Mock getElementById to track focus
const mockFocus = vi.fn()
const mockElement: Partial<HTMLElement> = { focus: mockFocus }
const originalQuerySelector = document.querySelector.bind(document)
const spy = vi
.spyOn(document, 'querySelector')
.mockImplementation((selector: string) => {
if (selector === '#comfy-org-sign-in-email')
return mockElement as HTMLElement
return originalQuerySelector(selector)
})
vi.spyOn(document, 'getElementById').mockReturnValue(
mockElement as HTMLElement
)
// Call handleForgotPassword with no email
await component.handleForgotPassword('', false)
// Should focus email input
expect(spy).toHaveBeenCalledWith('#comfy-org-sign-in-email')
expect(document.getElementById).toHaveBeenCalledWith(
'comfy-org-sign-in-email'
)
expect(mockFocus).toHaveBeenCalled()
spy.mockRestore()
})
it('does not focus email input when valid email is provided', async () => {

View File

@@ -120,7 +120,7 @@ const handleForgotPassword = async (
life: 5_000
})
// Focus the email input
document.querySelector<HTMLElement>(`#${emailInputId}`)?.focus?.()
document.getElementById(emailInputId)?.focus?.()
return
}
await firebaseAuthActions.sendPasswordReset(email)

View File

@@ -52,7 +52,7 @@ import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useDialogStore } from '@/stores/dialogStore'
const { cancelAt } = defineProps<{
const props = defineProps<{
cancelAt?: string
}>()
@@ -64,7 +64,7 @@ const { cancelSubscription, fetchStatus, subscription } = useBillingContext()
const isLoading = ref(false)
const formattedEndDate = computed(() => {
const dateStr = cancelAt ?? subscription.value?.endDate
const dateStr = props.cancelAt ?? subscription.value?.endDate
if (!dateStr) return t('subscription.cancelDialog.endOfBillingPeriod')
const date = new Date(dateStr)
return date.toLocaleDateString('en-US', {

View File

@@ -66,7 +66,7 @@ interface Props {
buttonStyles?: Record<string, string>
}
const { buttonStyles } = defineProps<Props>()
defineProps<Props>()
const buttonRef = ref<ComponentPublicInstance | null>(null)
const popover = ref<InstanceType<typeof Popover>>()
const commandStore = useCommandStore()

View File

@@ -65,12 +65,11 @@ const updateWidgets = () => {
const canvasStore = useCanvasStore()
whenever(
() => canvasStore.canvas,
(canvas) => {
canvas.onDrawForeground = useChainCallback(
(canvas) =>
(canvas.onDrawForeground = useChainCallback(
canvas.onDrawForeground,
updateWidgets
)
},
)),
{ immediate: true }
)
</script>

View File

@@ -162,8 +162,6 @@ import { useColorPaletteService } from '@/services/colorPaletteService'
import { useNewUserService } from '@/services/useNewUserService'
import { storeToRefs } from 'pinia'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { useBootstrapStore } from '@/stores/bootstrapStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
@@ -173,9 +171,11 @@ import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'
import { useInviteUrlLoader } from '@/platform/workspace/composables/useInviteUrlLoader'
import SelectionRectangle from './SelectionRectangle.vue'
import { isCloud } from '@/platform/distribution/types'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useInviteUrlLoader } from '@/platform/workspace/composables/useInviteUrlLoader'
const { t } = useI18n()
const emit = defineEmits<{
@@ -248,9 +248,9 @@ watch(
}
)
const allNodes = computed((): VueNodeData[] => [
...(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
])
const allNodes = computed((): VueNodeData[] =>
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
)
function onLinkOverlayReady(el: HTMLCanvasElement) {
if (!canvasStore.canvas) return

View File

@@ -114,8 +114,8 @@ const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
useZoomControls()
const stringifiedMinimapStyles = computed(() => {
const buttonGroupKeys = new Set(['borderRadius'])
const buttonKeys = new Set(['borderRadius'])
const buttonGroupKeys = ['borderRadius']
const buttonKeys = ['borderRadius']
const additionalButtonStyles = {
border: 'none'
}
@@ -124,12 +124,14 @@ const stringifiedMinimapStyles = computed(() => {
const buttonStyles = {
...Object.fromEntries(
Object.entries(containerStyles).filter(([key]) => buttonKeys.has(key))
Object.entries(containerStyles).filter(([key]) =>
buttonKeys.includes(key)
)
),
...additionalButtonStyles
}
const buttonGroupStyles = Object.entries(containerStyles)
.filter(([key]) => buttonGroupKeys.has(key))
.filter(([key]) => buttonGroupKeys.includes(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
return { buttonStyles, buttonGroupStyles }

View File

@@ -248,8 +248,8 @@ defineExpose({ toggle, hide, isOpen, show })
function showColorPopover(event: MouseEvent) {
event.stopPropagation()
event.preventDefault()
const target = [...(event.currentTarget as HTMLElement).children].find((el) =>
el.classList.contains('icon-[lucide--chevron-right]')
const target = Array.from((event.currentTarget as HTMLElement).children).find(
(el) => el.classList.contains('icon-[lucide--chevron-right]')
) as HTMLElement
colorPickerMenu.value?.toggle(event, target)
}

View File

@@ -34,14 +34,14 @@ const left = ref<string>()
const top = ref<string>()
function hideTooltip() {
tooltipText.value = ''
return (tooltipText.value = '')
}
async function showTooltip(tooltip: string | null | undefined) {
if (!tooltip) return
left.value = `${comfyApp.canvas.mouse[0]}px`
top.value = `${comfyApp.canvas.mouse[1]}px`
left.value = comfyApp.canvas.mouse[0] + 'px'
top.value = comfyApp.canvas.mouse[1] + 'px'
tooltipText.value = tooltip
await nextTick()
@@ -50,11 +50,11 @@ async function showTooltip(tooltip: string | null | undefined) {
if (!rect) return
if (rect.right > window.innerWidth) {
left.value = `${comfyApp.canvas.mouse[0] - rect.width}px`
left.value = comfyApp.canvas.mouse[0] - rect.width + 'px'
}
if (rect.top < 0) {
top.value = `${comfyApp.canvas.mouse[1] + rect.height}px`
top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'
}
}

View File

@@ -102,7 +102,7 @@ vi.mock('@/stores/nodeDefStore', () => ({
})
}))
describe(SelectionToolbox.__name ?? 'SelectionToolbox', () => {
describe('SelectionToolbox', () => {
let canvasStore: ReturnType<typeof useCanvasStore>
const i18n = createI18n({

View File

@@ -82,14 +82,16 @@ const { visible } = useSelectionToolboxPosition(toolboxRef)
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
const commandIds = new Set<string>(
canvasStore.selectedItems.flatMap(
(item) =>
extensionService
.invokeExtensions('getSelectionToolboxCommands', item)
.flat() as string[]
)
canvasStore.selectedItems
.map(
(item) =>
extensionService
.invokeExtensions('getSelectionToolboxCommands', item)
.flat() as string[]
)
.flat()
)
return [...commandIds]
return Array.from(commandIds)
.map((commandId) => commandStore.getCommand(commandId))
.filter((command): command is ComfyCommandImpl => command !== undefined)
})

View File

@@ -68,7 +68,7 @@ const createWrapper = (props = {}) => {
})
}
describe(ZoomControlsModal.__name ?? 'ZoomControlsModal', () => {
describe('ZoomControlsModal', () => {
beforeEach(() => {
vi.resetAllMocks()
})

View File

@@ -84,7 +84,7 @@ interface Props {
visible: boolean
}
const { visible } = defineProps<Props>()
const props = defineProps<Props>()
const interval = ref<number | null>(null)
@@ -132,7 +132,7 @@ const zoomToFitCommandText = computed(() =>
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => visible,
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()

View File

@@ -20,7 +20,7 @@ vi.mock('@/utils/litegraphUtil', () => ({
isLGraphNode: vi.fn(() => true)
}))
describe(BypassButton.__name ?? 'BypassButton', () => {
describe('BypassButton', () => {
let canvasStore: ReturnType<typeof useCanvasStore>
let commandStore: ReturnType<typeof useCommandStore>

View File

@@ -45,7 +45,7 @@ vi.mock('@/lib/litegraph/src/litegraph', async () => {
// Mock the colorUtil module
vi.mock('@/utils/colorUtil', () => ({
adjustColor: vi.fn((color: string) => `${color}_light`)
adjustColor: vi.fn((color: string) => color + '_light')
}))
// Mock the litegraphUtil module
@@ -56,7 +56,7 @@ vi.mock('@/utils/litegraphUtil', () => ({
isReroute: vi.fn(() => false)
}))
describe(ColorPickerButton.__name ?? 'ColorPickerButton', () => {
describe('ColorPickerButton', () => {
let canvasStore: ReturnType<typeof useCanvasStore>
let workflowStore: ReturnType<typeof useWorkflowStore>

View File

@@ -80,7 +80,7 @@ interface Emits {
(e: 'submenu-click', subOption: SubMenuOption): void
}
const { option } = defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { getCurrentShape } = useNodeCustomization()
@@ -113,9 +113,9 @@ const isShapeSelected = (subOption: SubMenuOption): boolean => {
const isColorSubmenu = computed(() => {
return (
option.submenu &&
option.submenu.length > 0 &&
option.submenu.every((item) => item.color && !item.icon)
props.option.submenu &&
props.option.submenu.length > 0 &&
props.option.submenu.every((item) => item.color && !item.icon)
)
})
</script>

View File

@@ -30,7 +30,7 @@ vi.mock('@/composables/graph/useSelectionState', () => ({
}))
}))
describe(ExecuteButton.__name ?? 'ExecuteButton', () => {
describe('ExecuteButton', () => {
let mockCanvas: LGraphCanvas
let mockSelectedNodes: LGraphNode[]

View File

@@ -24,7 +24,7 @@ import type { ComfyCommand } from '@/stores/commandStore'
import { useCommandStore } from '@/stores/commandStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
const { command } = defineProps<{
defineProps<{
command: ComfyCommand
}>()

View File

@@ -18,7 +18,7 @@ vi.mock('@/stores/workspace/rightSidePanelStore', () => ({
})
}))
describe(InfoButton.__name ?? 'InfoButton', () => {
describe('InfoButton', () => {
const i18n = createI18n({
legacy: false,
locale: 'en',

View File

@@ -20,7 +20,7 @@ import { useExecutionStore } from '@/stores/executionStore'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const modelValue = defineModel<string>({ required: true })
const { nodeId } = defineProps<{
const props = defineProps<{
nodeId: NodeId
}>()
@@ -30,7 +30,7 @@ const formattedText = computed(() => {
const src = modelValue.value
// Turn [[label|url]] into placeholders to avoid interfering with linkifyHtml
const tokens: { label: string; url: string }[] = []
const holed = src.replaceAll(
const holed = src.replace(
/\[\[([^|\]]+)\|([^\]]+)\]\]/g,
(_m, label, url) => {
tokens.push({ label: String(label), url: String(url) })
@@ -42,10 +42,10 @@ const formattedText = computed(() => {
let html = nl2br(linkifyHtml(holed))
// Restore placeholders as <a>...</a> (minimal escaping + http default)
html = html.replaceAll(/__LNK(\d+)__/g, (_m, i) => {
html = html.replace(/__LNK(\d+)__/g, (_m, i) => {
const { label, url } = tokens[+i]
const safeHref = url.replaceAll('"', '&quot;')
const safeLabel = label.replaceAll('<', '&lt;').replaceAll('>', '&gt;')
const safeHref = url.replace(/"/g, '&quot;')
const safeLabel = label.replace(/</g, '&lt;').replace(/>/g, '&gt;')
return /^https?:\/\//i.test(url)
? `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`
: safeLabel
@@ -58,7 +58,7 @@ let parentNodeId: NodeId | null = null
onMounted(() => {
// Get the parent node ID from props if provided
// For backward compatibility, fall back to the first executing node
parentNodeId = nodeId
parentNodeId = props.nodeId
})
// Watch for either a new node has starting execution or overall execution ending

View File

@@ -593,11 +593,11 @@ const onUpdateComfyUI = async (): Promise<void> => {
})
await rebootComfyUI()
} catch (error) {
} catch (err) {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: error instanceof Error ? error.message : t('g.unknownError'),
detail: err instanceof Error ? err.message : t('g.unknownError'),
life: 5000
})
}

View File

@@ -5,7 +5,7 @@ import { defineComponent, h, nextTick, ref } from 'vue'
import HoneyToast from './HoneyToast.vue'
describe(HoneyToast.__name ?? 'HoneyToast', () => {
describe('HoneyToast', () => {
beforeEach(() => {
vi.clearAllMocks()
document.body.innerHTML = ''

View File

@@ -110,7 +110,7 @@ import { ASPECT_RATIOS, useImageCrop } from '@/composables/useImageCrop'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { Bounds } from '@/renderer/core/layout/types'
const { nodeId } = defineProps<{
const props = defineProps<{
nodeId: NodeId
}>()
@@ -142,5 +142,5 @@ const {
handleResizeStart,
handleResizeMove,
handleResizeEnd
} = useImageCrop(nodeId, { imageEl, containerEl, modelValue })
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
</script>

View File

@@ -170,7 +170,7 @@ const getLabel = (val: string | null | undefined) => {
// Extract complex style logic from template
const optionStyle = computed(() => {
if (!popoverMinWidth && !popoverMaxWidth) return
if (!popoverMinWidth && !popoverMaxWidth) return undefined
const styles: string[] = []
if (popoverMinWidth) styles.push(`min-width: ${popoverMinWidth}`)

View File

@@ -84,24 +84,24 @@ import { app } from '@/scripts/app'
import type { ComponentWidget } from '@/scripts/domWidget'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const { widget, nodeId } = defineProps<{
const props = defineProps<{
widget: ComponentWidget<string[]> | SimplifiedWidget
nodeId?: NodeId
}>()
function isComponentWidget(
w: ComponentWidget<string[]> | SimplifiedWidget
): w is ComponentWidget<string[]> {
return 'node' in w && w.node !== undefined
widget: ComponentWidget<string[]> | SimplifiedWidget
): widget is ComponentWidget<string[]> {
return 'node' in widget && widget.node !== undefined
}
const node = ref<LGraphNode | null>(null)
if (isComponentWidget(widget)) {
node.value = widget.node
} else if (nodeId) {
if (isComponentWidget(props.widget)) {
node.value = props.widget.node
} else if (props.nodeId) {
onMounted(() => {
node.value = app.rootGraph?.getNodeById(nodeId!) || null
node.value = app.rootGraph?.getNodeById(props.nodeId!) || null
})
}

View File

@@ -35,14 +35,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
const {
initializeLoad3d,
cleanup,
loading,
loadingMessage,
onModelDrop,
isPreview
} = defineProps<{
const props = defineProps<{
initializeLoad3d: (containerRef: HTMLElement) => Promise<void>
cleanup: () => void
loading: boolean
@@ -60,20 +53,20 @@ function focusContainer() {
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
useLoad3dDrag({
onModelDrop: async (file) => {
if (onModelDrop) {
await onModelDrop(file)
if (props.onModelDrop) {
await props.onModelDrop(file)
}
},
disabled: computed(() => isPreview)
disabled: computed(() => props.isPreview)
})
onMounted(() => {
if (container.value) {
void initializeLoad3d(container.value)
void props.initializeLoad3d(container.value)
}
})
onUnmounted(() => {
cleanup()
props.cleanup()
})
</script>

View File

@@ -110,7 +110,7 @@ import { useLoad3dService } from '@/services/load3dService'
import { useDialogStore } from '@/stores/dialogStore'
const { t } = useI18n()
const { node, modelUrl } = defineProps<{
const props = defineProps<{
node?: LGraphNode
modelUrl?: string
}>()
@@ -120,10 +120,11 @@ const containerRef = ref<HTMLDivElement>()
const maximized = ref(false)
const mutationObserver = ref<MutationObserver | null>(null)
const isStandaloneMode = !node && modelUrl
const isStandaloneMode = !props.node && props.modelUrl
const viewer = node
? useLoad3dService().getOrCreateViewerSync(toRaw(node), useLoad3dViewer)
// Use sync version since useLoad3dViewer is already imported (module is loaded)
const viewer = props.node
? useLoad3dService().getOrCreateViewerSync(toRaw(props.node), useLoad3dViewer)
: useLoad3dViewer()
const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
@@ -137,10 +138,10 @@ const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
onMounted(async () => {
if (!containerRef.value) return
if (isStandaloneMode && modelUrl) {
await viewer.initializeStandaloneViewer(containerRef.value, modelUrl)
} else if (node) {
const source = useLoad3dService().getLoad3d(node)
if (isStandaloneMode && props.modelUrl) {
await viewer.initializeStandaloneViewer(containerRef.value, props.modelUrl)
} else if (props.node) {
const source = useLoad3dService().getLoad3d(props.node)
if (source) {
await viewer.initializeViewer(containerRef.value, source)
}

View File

@@ -69,7 +69,7 @@ const backgroundRenderMode = defineModel<'tiled' | 'panorama'>(
'backgroundRenderMode'
)
const { hasBackgroundImage, disableBackgroundUpload } = defineProps<{
defineProps<{
hasBackgroundImage?: boolean
disableBackgroundUpload?: boolean
}>()

View File

@@ -22,10 +22,10 @@ const currentPanelComponent = computed<Component>(() => {
if (tool === Tools.MaskBucket) {
return PaintBucketSettingsPanel
}
if (tool === Tools.MaskColorFill) {
} else if (tool === Tools.MaskColorFill) {
return ColorSelectSettingsPanel
} else {
return BrushSettingsPanel
}
return BrushSettingsPanel
})
</script>

View File

@@ -33,14 +33,14 @@ interface Props {
modelValue: string | number
}
const { label, options, modelValue } = defineProps<Props>()
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: string | number]
}>()
const normalizedOptions = computed((): DropdownOption[] => {
return options.map((option) => {
return props.options.map((option) => {
if (typeof option === 'string') {
return { label: option, value: option }
}

View File

@@ -24,7 +24,9 @@ interface Props {
modelValue: number
}
const { label, min, max, step = 1, modelValue } = defineProps<Props>()
withDefaults(defineProps<Props>(), {
step: 1
})
const emit = defineEmits<{
'update:modelValue': [value: number]

View File

@@ -21,7 +21,7 @@ interface Props {
modelValue: boolean
}
const { label, modelValue } = defineProps<Props>()
defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]

View File

@@ -10,7 +10,7 @@ import * as markdownRendererUtil from '@/utils/markdownRendererUtil'
import NodePreview from './NodePreview.vue'
describe(NodePreview.__name ?? 'NodePreview', () => {
describe('NodePreview', () => {
let i18n: ReturnType<typeof createI18n>
let pinia: ReturnType<typeof createPinia>

View File

@@ -32,7 +32,7 @@ const mountComponent = (props: Record<string, unknown>) =>
}
})
describe(CompletionSummaryBanner.__name ?? 'CompletionSummaryBanner', () => {
describe('CompletionSummaryBanner', () => {
it('renders success mode text, thumbnails, and aria label', () => {
const wrapper = mountComponent({
mode: 'allSuccess',
@@ -73,7 +73,7 @@ describe(CompletionSummaryBanner.__name ?? 'CompletionSummaryBanner', () => {
failedCount: 1
})
const summaryText = wrapper.text().replaceAll(/\s+/g, ' ').trim()
const summaryText = wrapper.text().replace(/\s+/g, ' ').trim()
expect(summaryText).toContain('2 jobs completed, 1 job failed')
})

View File

@@ -3,11 +3,11 @@
variant="secondary"
size="lg"
class="group w-full justify-between gap-3 p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="ariaLabel"
:aria-label="props.ariaLabel"
@click="emit('click', $event)"
>
<span class="inline-flex items-center gap-2">
<span v-if="mode === 'allFailed'" class="inline-flex items-center">
<span v-if="props.mode === 'allFailed'" class="inline-flex items-center">
<i
class="ml-1 icon-[lucide--circle-alert] block size-4 leading-none text-destructive-background"
/>
@@ -15,11 +15,11 @@
<span class="inline-flex items-center gap-2">
<span
v-if="mode !== 'allFailed'"
v-if="props.mode !== 'allFailed'"
class="relative inline-flex h-6 items-center"
>
<span
v-for="(url, idx) in thumbnailUrls"
v-for="(url, idx) in props.thumbnailUrls"
:key="url + idx"
class="inline-block h-6 w-6 overflow-hidden rounded-[6px] border-0 bg-secondary-background"
:style="{ marginLeft: idx === 0 ? '0' : '-12px' }"
@@ -33,42 +33,42 @@
</span>
<span class="text-[14px] font-normal text-text-primary">
<template v-if="mode === 'allSuccess'">
<template v-if="props.mode === 'allSuccess'">
<i18n-t
keypath="sideToolbar.queueProgressOverlay.jobsCompleted"
:plural="completedCount"
:plural="props.completedCount"
>
<template #count>
<span class="font-bold">{{ completedCount }}</span>
<span class="font-bold">{{ props.completedCount }}</span>
</template>
</i18n-t>
</template>
<template v-else-if="mode === 'mixed'">
<template v-else-if="props.mode === 'mixed'">
<i18n-t
keypath="sideToolbar.queueProgressOverlay.jobsCompleted"
:plural="completedCount"
:plural="props.completedCount"
>
<template #count>
<span class="font-bold">{{ completedCount }}</span>
<span class="font-bold">{{ props.completedCount }}</span>
</template>
</i18n-t>
<span>, </span>
<i18n-t
keypath="sideToolbar.queueProgressOverlay.jobsFailed"
:plural="failedCount"
:plural="props.failedCount"
>
<template #count>
<span class="font-bold">{{ failedCount }}</span>
<span class="font-bold">{{ props.failedCount }}</span>
</template>
</i18n-t>
</template>
<template v-else>
<i18n-t
keypath="sideToolbar.queueProgressOverlay.jobsFailed"
:plural="failedCount"
:plural="props.failedCount"
>
<template #count>
<span class="font-bold">{{ failedCount }}</span>
<span class="font-bold">{{ props.failedCount }}</span>
</template>
</i18n-t>
</template>
@@ -99,13 +99,9 @@ type Props = {
ariaLabel?: string
}
const {
mode,
completedCount,
failedCount,
thumbnailUrls = [],
ariaLabel
} = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
thumbnailUrls: () => []
})
const emit = defineEmits<{
(e: 'click', event: MouseEvent): void

Some files were not shown because too many files have changed in this diff Show More