mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 18:07:35 +00:00
Compare commits
27 Commits
queue_text
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3126fde24 | ||
|
|
1989561fb3 | ||
|
|
6f1f489e1a | ||
|
|
ce16b3a4eb | ||
|
|
6fd2051f5c | ||
|
|
06a36d6869 | ||
|
|
811ddd6165 | ||
|
|
0cdaa512c8 | ||
|
|
3a514ca63b | ||
|
|
405b5fc5b7 | ||
|
|
0eaf7d11b6 | ||
|
|
fa58c04b3a | ||
|
|
9c84c9e250 | ||
|
|
6f9f048b4a | ||
|
|
768faeee7e | ||
|
|
eba81efb4b | ||
|
|
f9d92b8198 | ||
|
|
c4bbe7fee1 | ||
|
|
8f4f5f8e5f | ||
|
|
9e137d9924 | ||
|
|
a084b55db7 | ||
|
|
835f318999 | ||
|
|
c35d44c491 | ||
|
|
38d3e15103 | ||
|
|
674d04c9cf | ||
|
|
8209765eec | ||
|
|
9d48638464 |
@@ -1,4 +1,3 @@
|
||||
|
||||
- use npm run to see what commands are available
|
||||
- After making code changes, follow this general process: (1) Create unit tests, component tests, browser tests (if appropriate for each), (2) run unit tests, component tests, and browser tests until passing, (3) run typecheck, lint, format (with prettier) -- you can use `npm run` command to see the scripts available, (4) check if any READMEs (including nested) or documentation needs to be updated, (5) Decide whether the changes are worth adding new content to the external documentation for (or would requires changes to the external documentation) at https://docs.comfy.org, then present your suggestion
|
||||
- When referencing PrimeVue, you can get all the docs here: https://primevue.org. Do this instead of making up or inferring names of Components
|
||||
@@ -36,3 +35,4 @@
|
||||
- Follow Vue 3 style guide and naming conventions
|
||||
- Use Vite for fast development and building
|
||||
- Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
|
||||
- Avoid using `@ts-expect-error` to work around type issues. We needed to employ it to migrate to TypeScript, but it should not be viewed as an accepted practice or standard.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 57 KiB |
@@ -32,7 +32,9 @@ test.describe('Templates', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('should have all required thumbnail media for each template', async ({
|
||||
// TODO: Re-enable this test once issue resolved
|
||||
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3992
|
||||
test.skip('should have all required thumbnail media for each template', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
test.slow()
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Plugin } from 'vite'
|
||||
|
||||
/**
|
||||
* Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode.
|
||||
*
|
||||
* This plugin addresses compatibility issues where some components or libraries
|
||||
* might be using the older createElementVNode function name instead of createBaseVNode.
|
||||
* It modifies the Vue vendor chunk during build to add the alias export.
|
||||
*
|
||||
* @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports
|
||||
*/
|
||||
export function addElementVnodeExportPlugin(): Plugin {
|
||||
return {
|
||||
name: 'add-element-vnode-export-plugin',
|
||||
|
||||
renderChunk(code, chunk, _options) {
|
||||
if (chunk.name.startsWith('vendor-vue')) {
|
||||
const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/
|
||||
const match = code.match(exportRegex)
|
||||
|
||||
if (match) {
|
||||
const existingExports = match[2].trim()
|
||||
const exportsArray = existingExports
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const hasCreateBaseVNode = exportsArray.some((e) =>
|
||||
e.startsWith('createBaseVNode')
|
||||
)
|
||||
const hasCreateElementVNode = exportsArray.some((e) =>
|
||||
e.includes('createElementVNode')
|
||||
)
|
||||
|
||||
if (hasCreateBaseVNode && !hasCreateElementVNode) {
|
||||
const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}`
|
||||
const newCode = code.replace(exportRegex, newExportStatement)
|
||||
|
||||
console.log(
|
||||
`[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.`
|
||||
)
|
||||
|
||||
return { code: newCode, map: null }
|
||||
} else if (!hasCreateBaseVNode) {
|
||||
console.warn(
|
||||
`[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,24 @@
|
||||
import type { OutputOptions } from 'rollup'
|
||||
import { HtmlTagDescriptor, Plugin } from 'vite'
|
||||
import glob from 'fast-glob'
|
||||
import fs from 'fs-extra'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite'
|
||||
|
||||
interface VendorLibrary {
|
||||
interface ImportMapSource {
|
||||
name: string
|
||||
pattern: RegExp
|
||||
pattern: string | RegExp
|
||||
entry: string
|
||||
recursiveDependence?: boolean
|
||||
override?: Record<string, Partial<ImportMapSource>>
|
||||
}
|
||||
|
||||
const parseDeps = (root: string, pkg: string) => {
|
||||
const pkgPath = join(root, 'node_modules', pkg, 'package.json')
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const content = fs.readFileSync(pkgPath, 'utf-8')
|
||||
const pkg = JSON.parse(content)
|
||||
return Object.keys(pkg.dependencies || {})
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,53 +38,89 @@ interface VendorLibrary {
|
||||
* @returns {Plugin} A Vite plugin that generates and injects an import map
|
||||
*/
|
||||
export function generateImportMapPlugin(
|
||||
vendorLibraries: VendorLibrary[]
|
||||
importMapSources: ImportMapSource[]
|
||||
): Plugin {
|
||||
const importMapEntries: Record<string, string> = {}
|
||||
const resolvedImportMapSources: Map<string, ImportMapSource> = new Map()
|
||||
const assetDir = 'assets/lib'
|
||||
let root: string
|
||||
|
||||
return {
|
||||
name: 'generate-import-map-plugin',
|
||||
|
||||
// Configure manual chunks during the build process
|
||||
configResolved(config) {
|
||||
root = config.root
|
||||
|
||||
if (config.build) {
|
||||
// Ensure rollupOptions exists
|
||||
if (!config.build.rollupOptions) {
|
||||
config.build.rollupOptions = {}
|
||||
}
|
||||
|
||||
const outputOptions: OutputOptions = {
|
||||
manualChunks: (id: string) => {
|
||||
for (const lib of vendorLibraries) {
|
||||
if (lib.pattern.test(id)) {
|
||||
return `vendor-${lib.name}`
|
||||
}
|
||||
for (const source of importMapSources) {
|
||||
resolvedImportMapSources.set(source.name, source)
|
||||
if (source.recursiveDependence) {
|
||||
const deps = parseDeps(root, source.name)
|
||||
|
||||
while (deps.length) {
|
||||
const dep = deps.shift()!
|
||||
const depSource = Object.assign({}, source, {
|
||||
name: dep,
|
||||
pattern: dep,
|
||||
...source.override?.[dep]
|
||||
})
|
||||
resolvedImportMapSources.set(depSource.name, depSource)
|
||||
|
||||
const _deps = parseDeps(root, depSource.name)
|
||||
deps.unshift(..._deps)
|
||||
}
|
||||
return null
|
||||
},
|
||||
// Disable minification of internal exports to preserve function names
|
||||
minifyInternalExports: false
|
||||
}
|
||||
}
|
||||
config.build.rollupOptions.output = outputOptions
|
||||
|
||||
const external: (string | RegExp)[] = []
|
||||
for (const [, source] of resolvedImportMapSources) {
|
||||
external.push(source.pattern)
|
||||
}
|
||||
config.build.rollupOptions.external = external
|
||||
}
|
||||
},
|
||||
|
||||
generateBundle(_options, bundle) {
|
||||
for (const fileName in bundle) {
|
||||
const chunk = bundle[fileName]
|
||||
if (chunk.type === 'chunk' && !chunk.isEntry) {
|
||||
// Find matching vendor library by chunk name
|
||||
const vendorLib = vendorLibraries.find(
|
||||
(lib) => chunk.name === `vendor-${lib.name}`
|
||||
)
|
||||
generateBundle(_options) {
|
||||
for (const [, source] of resolvedImportMapSources) {
|
||||
if (source.entry) {
|
||||
const moduleFile = join(source.name, source.entry)
|
||||
const sourceFile = join(root, 'node_modules', moduleFile)
|
||||
const targetFile = join(root, 'dist', assetDir, moduleFile)
|
||||
|
||||
if (vendorLib) {
|
||||
const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}`
|
||||
importMapEntries[vendorLib.name] = relativePath
|
||||
importMapEntries[source.name] =
|
||||
'./' + normalizePath(join(assetDir, moduleFile))
|
||||
|
||||
console.log(
|
||||
`[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'`
|
||||
)
|
||||
const targetDir = dirname(targetFile)
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true })
|
||||
}
|
||||
fs.copyFileSync(sourceFile, targetFile)
|
||||
}
|
||||
|
||||
if (source.recursiveDependence) {
|
||||
const files = glob.sync(['**/*.{js,mjs}'], {
|
||||
cwd: join(root, 'node_modules', source.name)
|
||||
})
|
||||
|
||||
for (const file of files) {
|
||||
const moduleFile = join(source.name, file)
|
||||
const sourceFile = join(root, 'node_modules', moduleFile)
|
||||
const targetFile = join(root, 'dist', assetDir, moduleFile)
|
||||
|
||||
importMapEntries[normalizePath(join(source.name, dirname(file)))] =
|
||||
'./' + normalizePath(join(assetDir, moduleFile))
|
||||
|
||||
const targetDir = dirname(targetFile)
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true })
|
||||
}
|
||||
fs.copyFileSync(sourceFile, targetFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin'
|
||||
export { comfyAPIPlugin } from './comfyAPIPlugin'
|
||||
export { generateImportMapPlugin } from './generateImportMapPlugin'
|
||||
|
||||
@@ -24,7 +24,7 @@ export default [
|
||||
},
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
project: ['./tsconfig.json', './tsconfig.eslint.json'],
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
extraFileExtensions: ['.vue']
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.15.11",
|
||||
"@comfyorg/litegraph": "^0.15.14",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -788,9 +788,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.15.11",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.11.tgz",
|
||||
"integrity": "sha512-gU8KK9cid7dXSK1yh3ReUolG0HGT3piKgKLd8YDr21PWl64pQvzy8BIh7W1vKH8ZWictKmNBaG9IRKlsJ667Zw==",
|
||||
"version": "0.15.14",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.14.tgz",
|
||||
"integrity": "sha512-9yERUwRVFPFspXowyg5z97QyF6+UbHG6ZNygvxSOisTCVSPOUeX/E02xcnhB5BHk0bTZCJGg9v2iztXBE5brnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.21.0",
|
||||
"version": "1.21.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.15.11",
|
||||
"@comfyorg/litegraph": "^0.15.14",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -30,6 +30,15 @@
|
||||
@click="download.triggerBrowserDownload"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
:label="$t('g.copyURL')"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="!!props.error"
|
||||
@click="copyURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,6 +47,7 @@ import Button from 'primevue/button'
|
||||
import Message from 'primevue/message'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { useDownload } from '@/composables/useDownload'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
@@ -49,9 +59,15 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const label = computed(() => props.label || props.url.split('/').pop())
|
||||
|
||||
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(props.url)
|
||||
}
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
</script>
|
||||
|
||||
293
src/components/dialog/content/signin/SignInForm.spec.ts
Normal file
293
src/components/dialog/content/signin/SignInForm.spec.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { Form } from '@primevue/forms'
|
||||
import { VueWrapper, mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import ToastService from 'primevue/toastservice'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
|
||||
import SignInForm from './SignInForm.vue'
|
||||
|
||||
type ComponentInstance = InstanceType<typeof SignInForm>
|
||||
|
||||
// Mock firebase auth modules
|
||||
vi.mock('firebase/app', () => ({
|
||||
initializeApp: vi.fn(),
|
||||
getApp: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
getAuth: vi.fn(),
|
||||
setPersistence: vi.fn(),
|
||||
browserLocalPersistence: {},
|
||||
onAuthStateChanged: vi.fn(),
|
||||
signInWithEmailAndPassword: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
sendPasswordResetEmail: vi.fn()
|
||||
}))
|
||||
|
||||
// Mock the auth composables and stores
|
||||
const mockSendPasswordReset = vi.fn()
|
||||
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
||||
useFirebaseAuthActions: vi.fn(() => ({
|
||||
sendPasswordReset: mockSendPasswordReset
|
||||
}))
|
||||
}))
|
||||
|
||||
let mockLoading = false
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
get loading() {
|
||||
return mockLoading
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock toast
|
||||
const mockToastAdd = vi.fn()
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: vi.fn(() => ({
|
||||
add: mockToastAdd
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('SignInForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockSendPasswordReset.mockReset()
|
||||
mockToastAdd.mockReset()
|
||||
mockLoading = false
|
||||
})
|
||||
|
||||
const mountComponent = (
|
||||
props = {},
|
||||
options = {}
|
||||
): VueWrapper<ComponentInstance> => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
return mount(SignInForm, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n, ToastService],
|
||||
components: {
|
||||
Form,
|
||||
Button,
|
||||
InputText,
|
||||
Password,
|
||||
ProgressSpinner
|
||||
}
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
describe('Forgot Password Link', () => {
|
||||
it('shows disabled style when email is empty', async () => {
|
||||
const wrapper = mountComponent()
|
||||
await nextTick()
|
||||
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
)
|
||||
|
||||
expect(forgotPasswordSpan.classes()).toContain('text-link-disabled')
|
||||
})
|
||||
|
||||
it('shows toast and focuses email input when clicked while disabled', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
)
|
||||
|
||||
// Mock getElementById to track focus
|
||||
const mockFocus = vi.fn()
|
||||
const mockElement = { focus: mockFocus }
|
||||
vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any)
|
||||
|
||||
// Click forgot password link while email is empty
|
||||
await forgotPasswordSpan.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// Should show toast warning
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'warn',
|
||||
summary: enMessages.auth.login.emailPlaceholder,
|
||||
life: 5000
|
||||
})
|
||||
|
||||
// Should focus email input
|
||||
expect(document.getElementById).toHaveBeenCalledWith(
|
||||
'comfy-org-sign-in-email'
|
||||
)
|
||||
expect(mockFocus).toHaveBeenCalled()
|
||||
|
||||
// Should NOT call sendPasswordReset
|
||||
expect(mockSendPasswordReset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls handleForgotPassword with email when link is clicked', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as any
|
||||
|
||||
// Spy on handleForgotPassword
|
||||
const handleForgotPasswordSpy = vi.spyOn(
|
||||
component,
|
||||
'handleForgotPassword'
|
||||
)
|
||||
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
)
|
||||
|
||||
// Click the forgot password link
|
||||
await forgotPasswordSpan.trigger('click')
|
||||
|
||||
// Should call handleForgotPassword
|
||||
expect(handleForgotPasswordSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Submission', () => {
|
||||
it('emits submit event when onSubmit is called with valid data', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as any
|
||||
|
||||
// Call onSubmit directly with valid data
|
||||
component.onSubmit({
|
||||
valid: true,
|
||||
values: { email: 'test@example.com', password: 'password123' }
|
||||
})
|
||||
|
||||
// Check emitted event
|
||||
expect(wrapper.emitted('submit')).toBeTruthy()
|
||||
expect(wrapper.emitted('submit')?.[0]).toEqual([
|
||||
{
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('does not emit submit event when form is invalid', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as any
|
||||
|
||||
// Call onSubmit with invalid form
|
||||
component.onSubmit({ valid: false, values: {} })
|
||||
|
||||
// Should not emit submit event
|
||||
expect(wrapper.emitted('submit')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('shows spinner when loading', async () => {
|
||||
mockLoading = true
|
||||
|
||||
try {
|
||||
const wrapper = mountComponent()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(false)
|
||||
} catch (error) {
|
||||
// Fallback test - check HTML content if component rendering fails
|
||||
mockLoading = true
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.html()).toContain('p-progressspinner')
|
||||
expect(wrapper.html()).not.toContain('<button')
|
||||
}
|
||||
})
|
||||
|
||||
it('shows button when not loading', () => {
|
||||
mockLoading = false
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(wrapper.findComponent(ProgressSpinner).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Structure', () => {
|
||||
it('renders email input with correct attributes', () => {
|
||||
const wrapper = mountComponent()
|
||||
const emailInput = wrapper.findComponent(InputText)
|
||||
|
||||
expect(emailInput.attributes('id')).toBe('comfy-org-sign-in-email')
|
||||
expect(emailInput.attributes('autocomplete')).toBe('email')
|
||||
expect(emailInput.attributes('name')).toBe('email')
|
||||
expect(emailInput.attributes('type')).toBe('text')
|
||||
})
|
||||
|
||||
it('renders password input with correct attributes', () => {
|
||||
const wrapper = mountComponent()
|
||||
const passwordInput = wrapper.findComponent(Password)
|
||||
|
||||
// Check props instead of attributes for Password component
|
||||
expect(passwordInput.props('inputId')).toBe('comfy-org-sign-in-password')
|
||||
// Password component passes name as prop, not attribute
|
||||
expect(passwordInput.props('name')).toBe('password')
|
||||
expect(passwordInput.props('feedback')).toBe(false)
|
||||
expect(passwordInput.props('toggleMask')).toBe(true)
|
||||
})
|
||||
|
||||
it('renders form with correct resolver', () => {
|
||||
const wrapper = mountComponent()
|
||||
const form = wrapper.findComponent(Form)
|
||||
|
||||
expect(form.props('resolver')).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Focus Behavior', () => {
|
||||
it('focuses email input when handleForgotPassword is called with invalid email', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as any
|
||||
|
||||
// Mock getElementById to track focus
|
||||
const mockFocus = vi.fn()
|
||||
const mockElement = { focus: mockFocus }
|
||||
vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any)
|
||||
|
||||
// Call handleForgotPassword with no email
|
||||
await component.handleForgotPassword('', false)
|
||||
|
||||
// Should focus email input
|
||||
expect(document.getElementById).toHaveBeenCalledWith(
|
||||
'comfy-org-sign-in-email'
|
||||
)
|
||||
expect(mockFocus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not focus email input when valid email is provided', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const component = wrapper.vm as any
|
||||
|
||||
// Mock getElementById
|
||||
const mockFocus = vi.fn()
|
||||
const mockElement = { focus: mockFocus }
|
||||
vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any)
|
||||
|
||||
// Call handleForgotPassword with valid email
|
||||
await component.handleForgotPassword('test@example.com', true)
|
||||
|
||||
// Should NOT focus email input
|
||||
expect(document.getElementById).not.toHaveBeenCalled()
|
||||
expect(mockFocus).not.toHaveBeenCalled()
|
||||
|
||||
// Should call sendPasswordReset
|
||||
expect(mockSendPasswordReset).toHaveBeenCalledWith('test@example.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -7,15 +7,12 @@
|
||||
>
|
||||
<!-- Email Field -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label
|
||||
class="opacity-80 text-base font-medium mb-2"
|
||||
for="comfy-org-sign-in-email"
|
||||
>
|
||||
<label class="opacity-80 text-base font-medium mb-2" :for="emailInputId">
|
||||
{{ t('auth.login.emailLabel') }}
|
||||
</label>
|
||||
<InputText
|
||||
pt:root:id="comfy-org-sign-in-email"
|
||||
pt:root:autocomplete="email"
|
||||
:id="emailInputId"
|
||||
autocomplete="email"
|
||||
class="h-10"
|
||||
name="email"
|
||||
type="text"
|
||||
@@ -37,8 +34,11 @@
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<span
|
||||
class="text-muted text-base font-medium cursor-pointer"
|
||||
@click="handleForgotPassword($form.email?.value)"
|
||||
class="text-muted text-base font-medium cursor-pointer select-none"
|
||||
:class="{
|
||||
'text-link-disabled': !$form.email?.value || $form.email?.invalid
|
||||
}"
|
||||
@click="handleForgotPassword($form.email?.value, $form.email?.valid)"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
</span>
|
||||
@@ -77,6 +77,7 @@ import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -87,6 +88,7 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
const loading = computed(() => authStore.loading)
|
||||
const toast = useToast()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -94,14 +96,34 @@ const emit = defineEmits<{
|
||||
submit: [values: SignInData]
|
||||
}>()
|
||||
|
||||
const emailInputId = 'comfy-org-sign-in-email'
|
||||
|
||||
const onSubmit = (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
emit('submit', event.values as SignInData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleForgotPassword = async (email: string) => {
|
||||
if (!email) return
|
||||
const handleForgotPassword = async (
|
||||
email: string,
|
||||
isValid: boolean | undefined
|
||||
) => {
|
||||
if (!email || !isValid) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: t('auth.login.emailPlaceholder'),
|
||||
life: 5_000
|
||||
})
|
||||
// Focus the email input
|
||||
document.getElementById(emailInputId)?.focus?.()
|
||||
return
|
||||
}
|
||||
await firebaseAuthActions.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-link-disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -150,8 +150,8 @@ const handleStopRecording = () => {
|
||||
if (load3DSceneRef.value?.load3d) {
|
||||
load3DSceneRef.value.load3d.stopRecording()
|
||||
isRecording.value = false
|
||||
hasRecording.value = true
|
||||
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,8 +294,8 @@ const listenRecordingStatusChange = (value: boolean) => {
|
||||
isRecording.value = value
|
||||
|
||||
if (!value && load3DSceneRef.value?.load3d) {
|
||||
hasRecording.value = true
|
||||
recordingDuration.value = load3DSceneRef.value.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,8 +168,8 @@ const handleStopRecording = () => {
|
||||
if (sceneRef?.load3d) {
|
||||
sceneRef.load3d.stopRecording()
|
||||
isRecording.value = false
|
||||
hasRecording.value = true
|
||||
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,8 +197,8 @@ const listenRecordingStatusChange = (value: boolean) => {
|
||||
if (!value) {
|
||||
const sceneRef = load3DAnimationSceneRef.value?.load3DSceneRef
|
||||
if (sceneRef?.load3d) {
|
||||
hasRecording.value = true
|
||||
recordingDuration.value = sceneRef.load3d.getRecordingDuration()
|
||||
hasRecording.value = recordingDuration.value > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,6 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const resizeNodeMatchOutput = () => {
|
||||
console.log('resizeNodeMatchOutput')
|
||||
|
||||
const outputWidth = node.widgets?.find((w) => w.name === 'width')
|
||||
const outputHeight = node.widgets?.find((w) => w.name === 'height')
|
||||
|
||||
|
||||
@@ -166,10 +166,11 @@ const showContextMenu = (e: CanvasPointerEvent) => {
|
||||
showSearchBox(e)
|
||||
}
|
||||
}
|
||||
const afterRerouteId = firstLink.fromReroute?.id
|
||||
const connectionOptions =
|
||||
toType === 'input'
|
||||
? { nodeFrom: node, slotFrom: fromSlot }
|
||||
: { nodeTo: node, slotTo: fromSlot }
|
||||
? { nodeFrom: node, slotFrom: fromSlot, afterRerouteId }
|
||||
: { nodeTo: node, slotTo: fromSlot, afterRerouteId }
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const menu = canvas.showConnectionMenu({
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
/>
|
||||
<ResultVideo v-else-if="item.isVideo" :result="item" />
|
||||
<ResultAudio v-else-if="item.isAudio" :result="item" />
|
||||
<ResultText v-else-if="item.isText" :result="item" />
|
||||
</template>
|
||||
</Galleria>
|
||||
</template>
|
||||
@@ -49,13 +48,12 @@ import ComfyImage from '@/components/common/ComfyImage.vue'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import ResultAudio from './ResultAudio.vue'
|
||||
import ResultText from './ResultText.vue'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const galleryVisible = ref(false)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:activeIndex': [number]
|
||||
(e: 'update:activeIndex', value: number): void
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
/>
|
||||
<ResultVideo v-else-if="result.isVideo" :result="result" />
|
||||
<ResultAudio v-else-if="result.isAudio" :result="result" />
|
||||
<ResultText v-else-if="result.isText" :result="result" />
|
||||
<div v-else class="task-result-preview">
|
||||
<i class="pi pi-file" />
|
||||
<span>{{ result.mediaType }}</span>
|
||||
@@ -29,7 +28,6 @@ import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import ResultAudio from './ResultAudio.vue'
|
||||
import ResultText from './ResultText.vue'
|
||||
import ResultVideo from './ResultVideo.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div class="result-text-container">
|
||||
<div class="text-content">
|
||||
{{ result.text }}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="copy-button"
|
||||
icon="pi pi-copy"
|
||||
text
|
||||
@click.stop="copyToClipboard(result.text ?? '')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
defineProps<{
|
||||
result: ResultItemImpl
|
||||
}>()
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-text-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
max-width: 80vw;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
padding-right: 0.5rem;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep functionality */
|
||||
.text-content::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
color: var(--text-color-secondary);
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--surface-ground);
|
||||
}
|
||||
|
||||
.result-text-container:hover .copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,16 @@
|
||||
import { type LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import {
|
||||
BaseWidget,
|
||||
type CanvasPointer,
|
||||
type LGraphNode,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
ICustomWidget,
|
||||
IWidgetOptions
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
|
||||
import { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
@@ -235,34 +240,61 @@ const renderPreview = (
|
||||
}
|
||||
}
|
||||
|
||||
class ImagePreviewWidget implements ICustomWidget {
|
||||
readonly type: 'custom'
|
||||
readonly name: string
|
||||
readonly options: IWidgetOptions<string | object>
|
||||
/** Dummy value to satisfy type requirements. */
|
||||
value: string
|
||||
y: number = 0
|
||||
/** Don't serialize the widget value. */
|
||||
serialize: boolean = false
|
||||
|
||||
constructor(name: string, options: IWidgetOptions<string | object>) {
|
||||
this.type = 'custom'
|
||||
this.name = name
|
||||
this.options = options
|
||||
this.value = ''
|
||||
}
|
||||
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
class ImagePreviewWidget extends BaseWidget {
|
||||
constructor(
|
||||
node: LGraphNode,
|
||||
_width: number,
|
||||
y: number,
|
||||
_height: number
|
||||
): void {
|
||||
renderPreview(ctx, node, y)
|
||||
name: string,
|
||||
options: IWidgetOptions<string | object>
|
||||
) {
|
||||
const widget: IBaseWidget = {
|
||||
name,
|
||||
options,
|
||||
type: 'custom',
|
||||
/** Dummy value to satisfy type requirements. */
|
||||
value: '',
|
||||
y: 0
|
||||
}
|
||||
super(widget, node)
|
||||
|
||||
// Don't serialize the widget value
|
||||
this.serialize = false
|
||||
}
|
||||
|
||||
computeLayoutSize(this: IBaseWidget) {
|
||||
override drawWidget(ctx: CanvasRenderingContext2D): void {
|
||||
renderPreview(ctx, this.node, this.y)
|
||||
}
|
||||
|
||||
override onPointerDown(pointer: CanvasPointer, node: LGraphNode): boolean {
|
||||
pointer.onDragStart = () => {
|
||||
const { canvas } = app
|
||||
const { graph } = canvas
|
||||
canvas.emitBeforeChange()
|
||||
graph?.beforeChange()
|
||||
// Ensure that dragging is properly cleaned up, on success or failure.
|
||||
pointer.finally = () => {
|
||||
canvas.isDragging = false
|
||||
graph?.afterChange()
|
||||
canvas.emitAfterChange()
|
||||
}
|
||||
|
||||
canvas.processSelect(node, pointer.eDown)
|
||||
canvas.isDragging = true
|
||||
}
|
||||
|
||||
pointer.onDragEnd = (e) => {
|
||||
const { canvas } = app
|
||||
if (e.shiftKey || LiteGraph.alwaysSnapToGrid)
|
||||
canvas.graph?.snapToGrid(canvas.selectedItems)
|
||||
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override onClick(): void {}
|
||||
|
||||
override computeLayoutSize() {
|
||||
return {
|
||||
minHeight: 220,
|
||||
minWidth: 1
|
||||
@@ -276,7 +308,7 @@ export const useImagePreviewWidget = () => {
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
return node.addCustomWidget(
|
||||
new ImagePreviewWidget(inputSpec.name, {
|
||||
new ImagePreviewWidget(node, inputSpec.name, {
|
||||
serialize: false
|
||||
})
|
||||
)
|
||||
|
||||
@@ -28,7 +28,8 @@ export const useTextPreviewWidget = (
|
||||
setValue: (value: string | object) => {
|
||||
widgetValue.value = typeof value === 'string' ? value : String(value)
|
||||
},
|
||||
getMinHeight: () => options.minHeight ?? 42 + PADDING
|
||||
getMinHeight: () => options.minHeight ?? 42 + PADDING,
|
||||
serialize: false
|
||||
}
|
||||
})
|
||||
addWidget(node, widget)
|
||||
|
||||
218
src/composables/widgets/useTextUploadWidget.ts
Normal file
218
src/composables/widgets/useTextUploadWidget.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { isComboWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import { useValueTransform } from '@/composables/useValueTransform'
|
||||
import { t } from '@/i18n'
|
||||
import type {
|
||||
CustomInputSpec,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { addToComboValues } from '@/utils/litegraphUtil'
|
||||
|
||||
const SUPPORTED_FILE_TYPES = ['text/plain', 'application/pdf', '.txt', '.pdf']
|
||||
const TEXT_FILE_UPLOAD = 'TEXT_FILE_UPLOAD'
|
||||
|
||||
const isTextFile = (file: File): boolean =>
|
||||
file.type === 'text/plain' || file.name.toLowerCase().endsWith('.txt')
|
||||
|
||||
const isPdfFile = (file: File): boolean =>
|
||||
file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
|
||||
|
||||
const isSupportedFile = (file: File): boolean =>
|
||||
isTextFile(file) || isPdfFile(file)
|
||||
|
||||
/**
|
||||
* Upload a text file and return the file path
|
||||
* @param file - The file to upload
|
||||
* @returns The file path or null if the upload fails
|
||||
*/
|
||||
async function uploadTextFile(file: File): Promise<string | null> {
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file) // Using standard field name for compatibility
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json()
|
||||
let path = data.name
|
||||
if (data.subfolder) {
|
||||
path = data.subfolder + '/' + path
|
||||
}
|
||||
return path
|
||||
} else {
|
||||
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().addAlert(String(error))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload multiple text files and return array of file paths
|
||||
* @param files - The files to upload
|
||||
* @returns The file paths or an empty array if the upload fails
|
||||
*/
|
||||
async function uploadTextFiles(files: File[]): Promise<string[]> {
|
||||
const uploadPromises = files.map(uploadTextFile)
|
||||
const results = await Promise.all(uploadPromises)
|
||||
return results.filter((path) => path !== null)
|
||||
}
|
||||
|
||||
interface TextUploadInputSpec extends CustomInputSpec {
|
||||
type: 'TEXT_FILE_UPLOAD'
|
||||
textInputName: string
|
||||
allow_batch?: boolean
|
||||
}
|
||||
|
||||
export const isTextUploadInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is TextUploadInputSpec => {
|
||||
return (
|
||||
inputSpec.type === TEXT_FILE_UPLOAD &&
|
||||
'textInputName' in inputSpec &&
|
||||
typeof inputSpec.textInputName === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a disabled button widget when text upload configuration is invalid
|
||||
*/
|
||||
const createDisabledUploadButton = (node: any, inputName: string) =>
|
||||
node.addWidget('button', inputName, '', () => {})
|
||||
|
||||
/**
|
||||
* Create a text upload widget using V2 input spec
|
||||
*/
|
||||
export const useTextUploadWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
node,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
if (!isTextUploadInputSpec(inputSpec)) {
|
||||
throw new Error(`Invalid input spec for text upload: ${inputSpec}`)
|
||||
}
|
||||
|
||||
// Get configuration from input spec
|
||||
const textInputName = inputSpec.textInputName
|
||||
const allow_batch = inputSpec.allow_batch === true
|
||||
|
||||
// Find the combo widget that will store the file path(s)
|
||||
const foundWidget = node.widgets?.find((w) => w.name === textInputName)
|
||||
|
||||
if (!foundWidget || !isComboWidget(foundWidget)) {
|
||||
console.error(
|
||||
`Text widget with name "${textInputName}" not found or is not a combo widget`
|
||||
)
|
||||
return createDisabledUploadButton(node, inputSpec.name)
|
||||
}
|
||||
|
||||
const textWidget = foundWidget
|
||||
|
||||
// Ensure options and values are initialized
|
||||
if (!textWidget.options) {
|
||||
textWidget.options = { values: [] }
|
||||
} else if (!textWidget.options.values) {
|
||||
textWidget.options.values = []
|
||||
}
|
||||
|
||||
// Types for internal handling
|
||||
type InternalValue = string | string[] | null | undefined
|
||||
type ExposedValue = string | string[]
|
||||
|
||||
const initialFile = allow_batch ? [] : ''
|
||||
|
||||
// Transform function to handle batch vs single file outputs
|
||||
const transform = (internalValue: InternalValue): ExposedValue => {
|
||||
if (!internalValue) return initialFile
|
||||
if (Array.isArray(internalValue))
|
||||
return allow_batch ? internalValue : internalValue[0] || ''
|
||||
return internalValue
|
||||
}
|
||||
|
||||
// Set up value transform on the widget
|
||||
Object.defineProperty(
|
||||
textWidget,
|
||||
'value',
|
||||
useValueTransform(transform, initialFile)
|
||||
)
|
||||
|
||||
// Handle the file upload
|
||||
const handleFileUpload = async (files: File[]): Promise<File[]> => {
|
||||
if (!files.length) return files
|
||||
|
||||
// Filter supported files
|
||||
const supportedFiles = files.filter(isSupportedFile)
|
||||
if (!supportedFiles.length) {
|
||||
useToastStore().addAlert(t('toastMessages.invalidFileType'))
|
||||
return files
|
||||
}
|
||||
|
||||
if (!allow_batch && supportedFiles.length > 1) {
|
||||
useToastStore().addAlert('Only single file upload is allowed')
|
||||
return files
|
||||
}
|
||||
|
||||
const filesToUpload = allow_batch ? supportedFiles : [supportedFiles[0]]
|
||||
const paths = await uploadTextFiles(filesToUpload)
|
||||
|
||||
if (paths.length && textWidget) {
|
||||
paths.forEach((path) => addToComboValues(textWidget, path))
|
||||
// @ts-expect-error litegraph combo value type does not support arrays yet
|
||||
textWidget.value = allow_batch ? paths : paths[0]
|
||||
if (textWidget.callback) {
|
||||
textWidget.callback(allow_batch ? paths : paths[0])
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// Set up file input for upload button
|
||||
const { openFileSelection } = useNodeFileInput(node, {
|
||||
accept: SUPPORTED_FILE_TYPES.join(','),
|
||||
allow_batch,
|
||||
onSelect: handleFileUpload
|
||||
})
|
||||
|
||||
// Set up drag and drop
|
||||
useNodeDragAndDrop(node, {
|
||||
fileFilter: isSupportedFile,
|
||||
onDrop: handleFileUpload
|
||||
})
|
||||
|
||||
// Set up paste
|
||||
useNodePaste(node, {
|
||||
fileFilter: isSupportedFile,
|
||||
allow_batch,
|
||||
onPaste: handleFileUpload
|
||||
})
|
||||
|
||||
// Create upload button widget
|
||||
const uploadWidget = node.addWidget(
|
||||
'button',
|
||||
inputSpec.name,
|
||||
'',
|
||||
openFileSelection,
|
||||
{ serialize: false }
|
||||
)
|
||||
|
||||
uploadWidget.label = allow_batch
|
||||
? t('g.choose_files_to_upload')
|
||||
: t('g.choose_file_to_upload')
|
||||
|
||||
return uploadWidget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
@@ -18,5 +18,6 @@ import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
import './uploadAudio'
|
||||
import './uploadImage'
|
||||
import './uploadText'
|
||||
import './webcamCapture'
|
||||
import './widgetInputs'
|
||||
|
||||
@@ -266,7 +266,7 @@ useExtensionService().registerExtension({
|
||||
LOAD_3D_ANIMATION(node) {
|
||||
const fileInput = document.createElement('input')
|
||||
fileInput.type = 'file'
|
||||
fileInput.accept = '.fbx,glb,gltf'
|
||||
fileInput.accept = '.gltf,.glb,.fbx'
|
||||
fileInput.style.display = 'none'
|
||||
fileInput.onchange = async () => {
|
||||
if (fileInput.files?.length) {
|
||||
@@ -452,31 +452,43 @@ useExtensionService().registerExtension({
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
onExecuted?.apply(this, arguments as any)
|
||||
useLoad3dService().waitForLoad3d(node, (load3d) => {
|
||||
const config = new Load3DConfiguration(load3d)
|
||||
|
||||
let filePath = message.result[0]
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
|
||||
|
||||
if (!filePath) {
|
||||
const msg = t('toastMessages.unableToGetModelFilePath')
|
||||
console.error(msg)
|
||||
useToastStore().addAlert(msg)
|
||||
}
|
||||
if (modelWidget) {
|
||||
const lastTimeModelFile = node.properties['Last Time Model File']
|
||||
|
||||
let cameraState = message.result[1]
|
||||
if (lastTimeModelFile) {
|
||||
modelWidget.value = lastTimeModelFile
|
||||
|
||||
useLoad3dService().waitForLoad3d(node, (load3d) => {
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
|
||||
|
||||
if (modelWidget) {
|
||||
modelWidget.value = filePath.replaceAll('\\', '/')
|
||||
|
||||
const config = new Load3DConfiguration(load3d)
|
||||
const cameraState = node.properties['Camera Info']
|
||||
|
||||
config.configure('output', modelWidget, cameraState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
onExecuted?.apply(this, arguments as any)
|
||||
|
||||
let filePath = message.result[0]
|
||||
|
||||
if (!filePath) {
|
||||
const msg = t('toastMessages.unableToGetModelFilePath')
|
||||
console.error(msg)
|
||||
useToastStore().addAlert(msg)
|
||||
}
|
||||
|
||||
let cameraState = message.result[1]
|
||||
|
||||
modelWidget.value = filePath.replaceAll('\\', '/')
|
||||
|
||||
node.properties['Last Time Model File'] = modelWidget.value
|
||||
|
||||
config.configure('output', modelWidget, cameraState)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -526,29 +538,42 @@ useExtensionService().registerExtension({
|
||||
|
||||
const onExecuted = node.onExecuted
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
onExecuted?.apply(this, arguments as any)
|
||||
useLoad3dService().waitForLoad3d(node, (load3d) => {
|
||||
const config = new Load3DConfiguration(load3d)
|
||||
|
||||
let filePath = message.result[0]
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
|
||||
|
||||
if (!filePath) {
|
||||
const msg = t('toastMessages.unableToGetModelFilePath')
|
||||
console.error(msg)
|
||||
useToastStore().addAlert(msg)
|
||||
}
|
||||
if (modelWidget) {
|
||||
const lastTimeModelFile = node.properties['Last Time Model File']
|
||||
|
||||
let cameraState = message.result[1]
|
||||
if (lastTimeModelFile) {
|
||||
modelWidget.value = lastTimeModelFile
|
||||
|
||||
useLoad3dService().waitForLoad3d(node, (load3d) => {
|
||||
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
|
||||
if (modelWidget) {
|
||||
modelWidget.value = filePath.replaceAll('\\', '/')
|
||||
|
||||
const config = new Load3DConfiguration(load3d)
|
||||
const cameraState = node.properties['Camera Info']
|
||||
|
||||
config.configure('output', modelWidget, cameraState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
node.onExecuted = function (message: any) {
|
||||
onExecuted?.apply(this, arguments as any)
|
||||
|
||||
let filePath = message.result[0]
|
||||
|
||||
if (!filePath) {
|
||||
const msg = t('toastMessages.unableToGetModelFilePath')
|
||||
console.error(msg)
|
||||
useToastStore().addAlert(msg)
|
||||
}
|
||||
|
||||
let cameraState = message.result[1]
|
||||
|
||||
modelWidget.value = filePath.replaceAll('\\', '/')
|
||||
|
||||
node.properties['Last Time Model File'] = modelWidget.value
|
||||
|
||||
config.configure('output', modelWidget, cameraState)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -132,6 +132,14 @@ export class LoaderManager implements LoaderManagerInterface {
|
||||
if (this.modelManager.materialMode === 'original') {
|
||||
const mtlUrl = url.replace(/(filename=.*?)\.obj/, '$1.mtl')
|
||||
|
||||
const subfolderMatch = url.match(/[?&]subfolder=([^&]*)/)
|
||||
|
||||
const subfolder = subfolderMatch
|
||||
? decodeURIComponent(subfolderMatch[1])
|
||||
: '3d'
|
||||
|
||||
this.mtlLoader.setSubfolder(subfolder)
|
||||
|
||||
try {
|
||||
const materials = await this.mtlLoader.loadAsync(mtlUrl)
|
||||
materials.preload()
|
||||
|
||||
@@ -38,6 +38,10 @@ class OverrideMTLLoader extends Loader {
|
||||
this.loadRootFolder = loadRootFolder
|
||||
}
|
||||
|
||||
setSubfolder(subfolder) {
|
||||
this.subfolder = subfolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts loading from the given URL and passes the loaded MTL asset
|
||||
* to the `onLoad()` callback.
|
||||
@@ -135,7 +139,8 @@ class OverrideMTLLoader extends Loader {
|
||||
const materialCreator = new OverrideMaterialCreator(
|
||||
this.resourcePath || path,
|
||||
this.materialOptions,
|
||||
this.loadRootFolder
|
||||
this.loadRootFolder,
|
||||
this.subfolder
|
||||
)
|
||||
materialCreator.setCrossOrigin(this.crossOrigin)
|
||||
materialCreator.setManager(this.manager)
|
||||
@@ -155,7 +160,7 @@ class OverrideMTLLoader extends Loader {
|
||||
*/
|
||||
|
||||
class OverrideMaterialCreator {
|
||||
constructor(baseUrl = '', options = {}, loadRootFolder) {
|
||||
constructor(baseUrl = '', options = {}, loadRootFolder, subfolder) {
|
||||
this.baseUrl = baseUrl
|
||||
this.options = options
|
||||
this.materialsInfo = {}
|
||||
@@ -164,6 +169,7 @@ class OverrideMaterialCreator {
|
||||
this.nameLookup = {}
|
||||
|
||||
this.loadRootFolder = loadRootFolder
|
||||
this.subfolder = subfolder
|
||||
|
||||
this.crossOrigin = 'anonymous'
|
||||
|
||||
@@ -283,16 +289,25 @@ class OverrideMaterialCreator {
|
||||
/**
|
||||
* Override for ComfyUI api url
|
||||
*/
|
||||
function resolveURL(baseUrl, url, loadRootFolder) {
|
||||
function resolveURL(baseUrl, url, loadRootFolder, subfolder) {
|
||||
if (typeof url !== 'string' || url === '') return ''
|
||||
|
||||
if (baseUrl.endsWith('/')) {
|
||||
baseUrl = baseUrl.slice(0, -1)
|
||||
}
|
||||
|
||||
if (!baseUrl.endsWith('api')) {
|
||||
baseUrl = '/api'
|
||||
}
|
||||
|
||||
baseUrl =
|
||||
baseUrl +
|
||||
'/view?filename=' +
|
||||
url +
|
||||
'&type=' +
|
||||
loadRootFolder +
|
||||
'&subfolder=3d'
|
||||
'&subfolder=' +
|
||||
subfolder
|
||||
|
||||
return baseUrl
|
||||
}
|
||||
@@ -302,7 +317,12 @@ class OverrideMaterialCreator {
|
||||
|
||||
const texParams = scope.getTextureParams(value, params)
|
||||
const map = scope.loadTexture(
|
||||
resolveURL(scope.baseUrl, texParams.url, scope.loadRootFolder)
|
||||
resolveURL(
|
||||
scope.baseUrl,
|
||||
texParams.url,
|
||||
scope.loadRootFolder,
|
||||
scope.subfolder
|
||||
)
|
||||
)
|
||||
|
||||
map.repeat.copy(texParams.scale)
|
||||
|
||||
50
src/extensions/core/uploadText.ts
Normal file
50
src/extensions/core/uploadText.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
ComfyNodeDef,
|
||||
InputSpec,
|
||||
isComboInputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Adds an upload button to text/pdf nodes
|
||||
|
||||
const isTextUploadComboInput = (inputSpec: InputSpec) => {
|
||||
const [inputName, inputOptions] = inputSpec
|
||||
if (!inputOptions) return false
|
||||
|
||||
const isUploadInput = inputOptions['text_file_upload'] === true
|
||||
|
||||
return (
|
||||
isUploadInput && (isComboInputSpecV1(inputSpec) || inputName === 'COMBO')
|
||||
)
|
||||
}
|
||||
|
||||
const createUploadInput = (
|
||||
textInputName: string,
|
||||
textInputOptions: InputSpec
|
||||
): InputSpec => [
|
||||
'TEXT_FILE_UPLOAD',
|
||||
{
|
||||
...textInputOptions[1],
|
||||
textInputName
|
||||
}
|
||||
]
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.UploadText',
|
||||
beforeRegisterNodeDef(_nodeType, nodeData: ComfyNodeDef) {
|
||||
const { input } = nodeData ?? {}
|
||||
const { required } = input ?? {}
|
||||
if (!required) return
|
||||
|
||||
const found = Object.entries(required).find(([_, input]) =>
|
||||
isTextUploadComboInput(input)
|
||||
)
|
||||
|
||||
// If text/pdf combo input found, attach upload input
|
||||
if (found) {
|
||||
const [inputName, inputSpec] = found
|
||||
required.upload = createUploadInput(inputName, inputSpec)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -86,6 +86,7 @@
|
||||
"control_after_generate": "control after generate",
|
||||
"control_before_generate": "control before generate",
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"choose_files_to_upload": "choose files to upload",
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"community": "Community",
|
||||
@@ -121,7 +122,8 @@
|
||||
"edit": "Edit",
|
||||
"copy": "Copy",
|
||||
"imageUrl": "Image URL",
|
||||
"clear": "Clear"
|
||||
"clear": "Clear",
|
||||
"copyURL": "Copy URL"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
@@ -1273,7 +1275,9 @@
|
||||
"failedToPurchaseCredits": "Failed to purchase credits: {error}",
|
||||
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist.",
|
||||
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
||||
"nothingSelected": "Nothing selected"
|
||||
"nothingSelected": "Nothing selected",
|
||||
"invalidFileType": "Invalid file type",
|
||||
"onlySingleFile": "Only single file upload is allowed"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "captura",
|
||||
"category": "Categoría",
|
||||
"choose_file_to_upload": "elige archivo para subir",
|
||||
"choose_files_to_upload": "elige archivos para subir",
|
||||
"clear": "Limpiar",
|
||||
"close": "Cerrar",
|
||||
"color": "Color",
|
||||
@@ -268,6 +269,7 @@
|
||||
"control_before_generate": "control antes de generar",
|
||||
"copy": "Copiar",
|
||||
"copyToClipboard": "Copiar al portapapeles",
|
||||
"copyURL": "Copiar URL",
|
||||
"currentUser": "Usuario actual",
|
||||
"customBackground": "Fondo personalizado",
|
||||
"customize": "Personalizar",
|
||||
@@ -1348,6 +1350,7 @@
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
"fileUploadFailed": "Error al subir el archivo",
|
||||
"interrupted": "La ejecución ha sido interrumpida",
|
||||
"invalidFileType": "Tipo de archivo no válido",
|
||||
"migrateToLitegraphReroute": "Los nodos de reroute se eliminarán en futuras versiones. Haz clic para migrar a reroute nativo de litegraph.",
|
||||
"no3dScene": "No hay escena 3D para aplicar textura",
|
||||
"no3dSceneToExport": "No hay escena 3D para exportar",
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"nothingSelected": "Nada seleccionado",
|
||||
"nothingToGroup": "Nada para agrupar",
|
||||
"nothingToQueue": "Nada para poner en cola",
|
||||
"onlySingleFile": "Solo se permite subir un archivo",
|
||||
"pendingTasksDeleted": "Tareas pendientes eliminadas",
|
||||
"pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para",
|
||||
"pleaseSelectOutputNodes": "Por favor, selecciona los nodos de salida",
|
||||
|
||||
@@ -3403,7 +3403,7 @@
|
||||
"clear": {
|
||||
},
|
||||
"height": {
|
||||
"name": "altura"
|
||||
"name": "alto"
|
||||
},
|
||||
"image": {
|
||||
"name": "imagen"
|
||||
@@ -3417,20 +3417,26 @@
|
||||
"name": "ancho"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "imagen"
|
||||
},
|
||||
"1": {
|
||||
"name": "mask"
|
||||
},
|
||||
"2": {
|
||||
"name": "ruta_malla"
|
||||
},
|
||||
"3": {
|
||||
"name": "normal"
|
||||
},
|
||||
{
|
||||
"4": {
|
||||
"name": "lineart"
|
||||
},
|
||||
{
|
||||
"name": "camera_info"
|
||||
"5": {
|
||||
"name": "info_cámara"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Load3DAnimation": {
|
||||
"display_name": "Cargar 3D - Animación",
|
||||
@@ -3438,7 +3444,7 @@
|
||||
"clear": {
|
||||
},
|
||||
"height": {
|
||||
"name": "altura"
|
||||
"name": "alto"
|
||||
},
|
||||
"image": {
|
||||
"name": "imagen"
|
||||
@@ -3452,17 +3458,23 @@
|
||||
"name": "ancho"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "imagen"
|
||||
},
|
||||
"1": {
|
||||
"name": "mask"
|
||||
},
|
||||
"2": {
|
||||
"name": "ruta_malla"
|
||||
},
|
||||
"3": {
|
||||
"name": "normal"
|
||||
},
|
||||
{
|
||||
"name": "camera_info"
|
||||
"4": {
|
||||
"name": "info_cámara"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LoadAudio": {
|
||||
"display_name": "CargarAudio",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "capture",
|
||||
"category": "Catégorie",
|
||||
"choose_file_to_upload": "choisissez le fichier à télécharger",
|
||||
"choose_files_to_upload": "choisissez des fichiers à télécharger",
|
||||
"clear": "Effacer",
|
||||
"close": "Fermer",
|
||||
"color": "Couleur",
|
||||
@@ -268,6 +269,7 @@
|
||||
"control_before_generate": "contrôle avant génération",
|
||||
"copy": "Copier",
|
||||
"copyToClipboard": "Copier dans le presse-papiers",
|
||||
"copyURL": "Copier l’URL",
|
||||
"currentUser": "Utilisateur actuel",
|
||||
"customBackground": "Arrière-plan personnalisé",
|
||||
"customize": "Personnaliser",
|
||||
@@ -1348,6 +1350,7 @@
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
"fileUploadFailed": "Échec du téléchargement du fichier",
|
||||
"interrupted": "L'exécution a été interrompue",
|
||||
"invalidFileType": "Type de fichier invalide",
|
||||
"migrateToLitegraphReroute": "Les nœuds de reroute seront supprimés dans les futures versions. Cliquez pour migrer vers le reroute natif de litegraph.",
|
||||
"no3dScene": "Aucune scène 3D pour appliquer la texture",
|
||||
"no3dSceneToExport": "Aucune scène 3D à exporter",
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"nothingSelected": "Aucune sélection",
|
||||
"nothingToGroup": "Rien à regrouper",
|
||||
"nothingToQueue": "Rien à ajouter à la file d’attente",
|
||||
"onlySingleFile": "Le téléchargement d’un seul fichier est autorisé",
|
||||
"pendingTasksDeleted": "Tâches en attente supprimées",
|
||||
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
|
||||
"pleaseSelectOutputNodes": "Veuillez sélectionner les nœuds de sortie",
|
||||
|
||||
@@ -3417,20 +3417,26 @@
|
||||
"name": "largeur"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "image"
|
||||
},
|
||||
"1": {
|
||||
"name": "masque"
|
||||
},
|
||||
"2": {
|
||||
"name": "chemin_maillage"
|
||||
},
|
||||
"3": {
|
||||
"name": "normale"
|
||||
},
|
||||
{
|
||||
"name": "ligne artistique"
|
||||
"4": {
|
||||
"name": "lineart"
|
||||
},
|
||||
{
|
||||
"name": "informations caméra"
|
||||
"5": {
|
||||
"name": "info_caméra"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Load3DAnimation": {
|
||||
"display_name": "Charger 3D - Animation",
|
||||
@@ -3452,17 +3458,23 @@
|
||||
"name": "largeur"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"name": "normal"
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "image"
|
||||
},
|
||||
{
|
||||
"name": "camera_info"
|
||||
"1": {
|
||||
"name": "masque"
|
||||
},
|
||||
"2": {
|
||||
"name": "chemin_maillage"
|
||||
},
|
||||
"3": {
|
||||
"name": "normale"
|
||||
},
|
||||
"4": {
|
||||
"name": "info_caméra"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LoadAudio": {
|
||||
"display_name": "ChargerAudio",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "キャプチャ",
|
||||
"category": "カテゴリ",
|
||||
"choose_file_to_upload": "アップロードするファイルを選択",
|
||||
"choose_files_to_upload": "アップロードするファイルを選択",
|
||||
"clear": "クリア",
|
||||
"close": "閉じる",
|
||||
"color": "色",
|
||||
@@ -268,6 +269,7 @@
|
||||
"control_before_generate": "生成前の制御",
|
||||
"copy": "コピー",
|
||||
"copyToClipboard": "クリップボードにコピー",
|
||||
"copyURL": "URLをコピー",
|
||||
"currentUser": "現在のユーザー",
|
||||
"customBackground": "カスタム背景",
|
||||
"customize": "カスタマイズ",
|
||||
@@ -1348,6 +1350,7 @@
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
"fileUploadFailed": "ファイルのアップロードに失敗しました",
|
||||
"interrupted": "実行が中断されました",
|
||||
"invalidFileType": "無効なファイルタイプです",
|
||||
"migrateToLitegraphReroute": "将来のバージョンではRerouteノードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。",
|
||||
"no3dScene": "テクスチャを適用する3Dシーンがありません",
|
||||
"no3dSceneToExport": "エクスポートする3Dシーンがありません",
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"nothingSelected": "選択されていません",
|
||||
"nothingToGroup": "グループ化するものがありません",
|
||||
"nothingToQueue": "キューに追加する項目がありません",
|
||||
"onlySingleFile": "ファイルは1つだけアップロードできます",
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
|
||||
"pleaseSelectOutputNodes": "出力ノードを選択してください",
|
||||
|
||||
@@ -3417,23 +3417,29 @@
|
||||
"name": "幅"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "画像"
|
||||
},
|
||||
"1": {
|
||||
"name": "マスク"
|
||||
},
|
||||
"2": {
|
||||
"name": "メッシュパス"
|
||||
},
|
||||
"3": {
|
||||
"name": "法線"
|
||||
},
|
||||
{
|
||||
"4": {
|
||||
"name": "線画"
|
||||
},
|
||||
{
|
||||
"5": {
|
||||
"name": "カメラ情報"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Load3DAnimation": {
|
||||
"display_name": "3Dを読み込む - アニメーション",
|
||||
"display_name": "3D読み込み - アニメーション",
|
||||
"inputs": {
|
||||
"clear": {
|
||||
},
|
||||
@@ -3452,17 +3458,23 @@
|
||||
"name": "幅"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "画像"
|
||||
},
|
||||
"1": {
|
||||
"name": "マスク"
|
||||
},
|
||||
"2": {
|
||||
"name": "メッシュパス"
|
||||
},
|
||||
"3": {
|
||||
"name": "法線"
|
||||
},
|
||||
{
|
||||
"4": {
|
||||
"name": "カメラ情報"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LoadAudio": {
|
||||
"display_name": "音声を読み込む",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "캡처",
|
||||
"category": "카테고리",
|
||||
"choose_file_to_upload": "업로드할 파일 선택",
|
||||
"choose_files_to_upload": "업로드할 파일 선택",
|
||||
"clear": "지우기",
|
||||
"close": "닫기",
|
||||
"color": "색상",
|
||||
@@ -268,6 +269,7 @@
|
||||
"control_before_generate": "생성 전 제어",
|
||||
"copy": "복사",
|
||||
"copyToClipboard": "클립보드에 복사",
|
||||
"copyURL": "URL 복사",
|
||||
"currentUser": "현재 사용자",
|
||||
"customBackground": "맞춤 배경",
|
||||
"customize": "사용자 정의",
|
||||
@@ -1348,6 +1350,7 @@
|
||||
"fileLoadError": "{fileName}에서 워크플로우를 찾을 수 없습니다",
|
||||
"fileUploadFailed": "파일 업로드에 실패했습니다",
|
||||
"interrupted": "실행이 중단되었습니다",
|
||||
"invalidFileType": "잘못된 파일 형식입니다",
|
||||
"migrateToLitegraphReroute": "향후 버전에서는 Reroute 노드가 제거됩니다. LiteGraph 에서 자체 제공하는 경유점으로 변환하려면 클릭하세요.",
|
||||
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
|
||||
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"nothingSelected": "선택된 항목이 없습니다",
|
||||
"nothingToGroup": "그룹화할 항목이 없습니다",
|
||||
"nothingToQueue": "대기열에 추가할 항목이 없습니다",
|
||||
"onlySingleFile": "파일은 하나만 업로드할 수 있습니다",
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
|
||||
"pleaseSelectOutputNodes": "출력 노드를 선택해 주세요",
|
||||
|
||||
@@ -3398,7 +3398,7 @@
|
||||
}
|
||||
},
|
||||
"Load3D": {
|
||||
"display_name": "3D 로드",
|
||||
"display_name": "3D 불러오기",
|
||||
"inputs": {
|
||||
"clear": {
|
||||
},
|
||||
@@ -3417,23 +3417,29 @@
|
||||
"name": "너비"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "이미지"
|
||||
},
|
||||
"1": {
|
||||
"name": "마스크"
|
||||
},
|
||||
"2": {
|
||||
"name": "메시 경로"
|
||||
},
|
||||
"3": {
|
||||
"name": "노멀"
|
||||
},
|
||||
{
|
||||
"4": {
|
||||
"name": "라인아트"
|
||||
},
|
||||
{
|
||||
"5": {
|
||||
"name": "카메라 정보"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Load3DAnimation": {
|
||||
"display_name": "3D 로드 - 애니메이션",
|
||||
"display_name": "3D 불러오기 - 애니메이션",
|
||||
"inputs": {
|
||||
"clear": {
|
||||
},
|
||||
@@ -3452,17 +3458,23 @@
|
||||
"name": "너비"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "이미지"
|
||||
},
|
||||
"1": {
|
||||
"name": "마스크"
|
||||
},
|
||||
"2": {
|
||||
"name": "메시 경로"
|
||||
},
|
||||
"3": {
|
||||
"name": "노멀"
|
||||
},
|
||||
{
|
||||
"4": {
|
||||
"name": "카메라 정보"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LoadAudio": {
|
||||
"display_name": "오디오 로드",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "захват",
|
||||
"category": "Категория",
|
||||
"choose_file_to_upload": "выберите файл для загрузки",
|
||||
"choose_files_to_upload": "выберите файлы для загрузки",
|
||||
"clear": "Очистить",
|
||||
"close": "Закрыть",
|
||||
"color": "Цвет",
|
||||
@@ -268,6 +269,7 @@
|
||||
"control_before_generate": "управление до генерации",
|
||||
"copy": "Копировать",
|
||||
"copyToClipboard": "Скопировать в буфер обмена",
|
||||
"copyURL": "Скопировать URL",
|
||||
"currentUser": "Текущий пользователь",
|
||||
"customBackground": "Пользовательский фон",
|
||||
"customize": "Настроить",
|
||||
@@ -1348,6 +1350,7 @@
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
"fileUploadFailed": "Не удалось загрузить файл",
|
||||
"interrupted": "Выполнение было прервано",
|
||||
"invalidFileType": "Недопустимый тип файла",
|
||||
"migrateToLitegraphReroute": "Узлы перенаправления будут удалены в будущих версиях. Нажмите, чтобы перейти на litegraph-native reroute.",
|
||||
"no3dScene": "Нет 3D сцены для применения текстуры",
|
||||
"no3dSceneToExport": "Нет 3D сцены для экспорта",
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"nothingSelected": "Ничего не выбрано",
|
||||
"nothingToGroup": "Нечего группировать",
|
||||
"nothingToQueue": "Нет заданий в очереди",
|
||||
"onlySingleFile": "Разрешена загрузка только одного файла",
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
|
||||
"pleaseSelectOutputNodes": "Пожалуйста, выберите выходные узлы",
|
||||
|
||||
@@ -3409,7 +3409,7 @@
|
||||
"name": "изображение"
|
||||
},
|
||||
"model_file": {
|
||||
"name": "файл_модели"
|
||||
"name": "файл модели"
|
||||
},
|
||||
"upload 3d model": {
|
||||
},
|
||||
@@ -3417,23 +3417,29 @@
|
||||
"name": "ширина"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "изображение"
|
||||
},
|
||||
"1": {
|
||||
"name": "mask"
|
||||
},
|
||||
"2": {
|
||||
"name": "путь к mesh"
|
||||
},
|
||||
"3": {
|
||||
"name": "нормаль"
|
||||
},
|
||||
{
|
||||
"name": "линеарт"
|
||||
"4": {
|
||||
"name": "линейный рисунок"
|
||||
},
|
||||
{
|
||||
"name": "информация_камеры"
|
||||
"5": {
|
||||
"name": "информация о камере"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Load3DAnimation": {
|
||||
"display_name": "Загрузить 3D — Анимация",
|
||||
"display_name": "Загрузить 3D - Анимация",
|
||||
"inputs": {
|
||||
"clear": {
|
||||
},
|
||||
@@ -3452,17 +3458,23 @@
|
||||
"name": "ширина"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "изображение"
|
||||
},
|
||||
"1": {
|
||||
"name": "mask"
|
||||
},
|
||||
"2": {
|
||||
"name": "путь_к_модели"
|
||||
},
|
||||
"3": {
|
||||
"name": "нормаль"
|
||||
},
|
||||
{
|
||||
"name": "информация_камеры"
|
||||
"4": {
|
||||
"name": "информация_о_камере"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LoadAudio": {
|
||||
"display_name": "Загрузить аудио",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "捕获",
|
||||
"category": "类别",
|
||||
"choose_file_to_upload": "选择要上传的文件",
|
||||
"choose_files_to_upload": "选择要上传的文件",
|
||||
"clear": "清除",
|
||||
"close": "关闭",
|
||||
"color": "颜色",
|
||||
@@ -268,6 +269,7 @@
|
||||
"control_before_generate": "生成前控制",
|
||||
"copy": "复制",
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"copyURL": "复制链接",
|
||||
"currentUser": "当前用户",
|
||||
"customBackground": "自定义背景",
|
||||
"customize": "自定义",
|
||||
@@ -1348,6 +1350,7 @@
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
"fileUploadFailed": "文件上传失败",
|
||||
"interrupted": "执行已被中断",
|
||||
"invalidFileType": "文件类型无效",
|
||||
"migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。",
|
||||
"no3dScene": "没有3D场景可以应用纹理",
|
||||
"no3dSceneToExport": "没有3D场景可以导出",
|
||||
@@ -1356,6 +1359,7 @@
|
||||
"nothingSelected": "未选择任何内容",
|
||||
"nothingToGroup": "没有可分组的内容",
|
||||
"nothingToQueue": "没有可加入队列的内容",
|
||||
"onlySingleFile": "只允许上传单个文件",
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
|
||||
"pleaseSelectOutputNodes": "请选择输出节点",
|
||||
|
||||
@@ -3417,20 +3417,26 @@
|
||||
"name": "宽度"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"name": "法线"
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "image"
|
||||
},
|
||||
{
|
||||
"name": "线稿"
|
||||
"1": {
|
||||
"name": "mask"
|
||||
},
|
||||
{
|
||||
"name": "相机信息"
|
||||
"2": {
|
||||
"name": "mesh_path"
|
||||
},
|
||||
"3": {
|
||||
"name": "normal"
|
||||
},
|
||||
"4": {
|
||||
"name": "lineart"
|
||||
},
|
||||
"5": {
|
||||
"name": "camera_info"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Load3DAnimation": {
|
||||
"display_name": "加载3D动画",
|
||||
@@ -3452,17 +3458,23 @@
|
||||
"name": "宽度"
|
||||
}
|
||||
},
|
||||
"outputs": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "图像"
|
||||
},
|
||||
"1": {
|
||||
"name": "遮罩"
|
||||
},
|
||||
"2": {
|
||||
"name": "mesh_path"
|
||||
},
|
||||
"3": {
|
||||
"name": "法线"
|
||||
},
|
||||
{
|
||||
"4": {
|
||||
"name": "相机信息"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"LoadAudio": {
|
||||
"display_name": "加载音频",
|
||||
|
||||
@@ -22,8 +22,7 @@ const zOutputs = z
|
||||
audio: z.array(zResultItem).optional(),
|
||||
images: z.array(zResultItem).optional(),
|
||||
video: z.array(zResultItem).optional(),
|
||||
animated: z.array(z.boolean()).optional(),
|
||||
text: z.string().optional()
|
||||
animated: z.array(z.boolean()).optional()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ export const zComboInputOptions = zBaseInputOptions.extend({
|
||||
allow_batch: z.boolean().optional(),
|
||||
video_upload: z.boolean().optional(),
|
||||
animated_image_upload: z.boolean().optional(),
|
||||
text_file_upload: z.boolean().optional(),
|
||||
options: z.array(zComboOption).optional(),
|
||||
remote: zRemoteWidgetConfig.optional(),
|
||||
/** Whether the widget is a multi-select widget. */
|
||||
|
||||
@@ -1074,11 +1074,11 @@ export class ComfyApp {
|
||||
try {
|
||||
// @ts-expect-error Discrepancies between zod and litegraph - in progress
|
||||
this.graph.configure(graphData)
|
||||
if (restore_view) {
|
||||
if (
|
||||
useSettingStore().get('Comfy.EnableWorkflowViewRestore') &&
|
||||
graphData.extra?.ds
|
||||
) {
|
||||
if (
|
||||
restore_view &&
|
||||
useSettingStore().get('Comfy.EnableWorkflowViewRestore')
|
||||
) {
|
||||
if (graphData.extra?.ds) {
|
||||
this.canvas.ds.offset = graphData.extra.ds.offset
|
||||
this.canvas.ds.scale = graphData.extra.ds.scale
|
||||
} else {
|
||||
|
||||
@@ -32,7 +32,7 @@ export class ChangeTracker {
|
||||
/**
|
||||
* Whether the redo/undo restoring is in progress.
|
||||
*/
|
||||
private restoringState: boolean = false
|
||||
_restoringState: boolean = false
|
||||
|
||||
ds?: { scale: number; offset: [number, number] }
|
||||
nodeOutputs?: Record<string, any>
|
||||
@@ -55,7 +55,7 @@ export class ChangeTracker {
|
||||
*/
|
||||
reset(state?: ComfyWorkflowJSON) {
|
||||
// Do not reset the state if we are restoring.
|
||||
if (this.restoringState) return
|
||||
if (this._restoringState) return
|
||||
|
||||
logger.debug('Reset State')
|
||||
if (state) this.activeState = clone(state)
|
||||
@@ -124,7 +124,7 @@ export class ChangeTracker {
|
||||
const prevState = source.pop()
|
||||
if (prevState) {
|
||||
target.push(this.activeState)
|
||||
this.restoringState = true
|
||||
this._restoringState = true
|
||||
try {
|
||||
await app.loadGraphData(prevState, false, false, this.workflow, {
|
||||
showMissingModelsDialog: false,
|
||||
@@ -134,7 +134,7 @@ export class ChangeTracker {
|
||||
this.activeState = prevState
|
||||
this.updateModified()
|
||||
} finally {
|
||||
this.restoringState = false
|
||||
this._restoringState = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useImageUploadWidget } from '@/composables/widgets/useImageUploadWidget
|
||||
import { useIntWidget } from '@/composables/widgets/useIntWidget'
|
||||
import { useMarkdownWidget } from '@/composables/widgets/useMarkdownWidget'
|
||||
import { useStringWidget } from '@/composables/widgets/useStringWidget'
|
||||
import { useTextUploadWidget } from '@/composables/widgets/useTextUploadWidget'
|
||||
import { t } from '@/i18n'
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
@@ -289,5 +290,6 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
|
||||
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
|
||||
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
|
||||
IMAGEUPLOAD: useImageUploadWidget()
|
||||
IMAGEUPLOAD: useImageUploadWidget(),
|
||||
TEXT_FILE_UPLOAD: transformWidgetConstructorV2ToV1(useTextUploadWidget())
|
||||
}
|
||||
|
||||
@@ -379,6 +379,24 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog from a third party extension.
|
||||
* @param options - The dialog options.
|
||||
* @param options.key - The dialog key.
|
||||
* @param options.title - The dialog title.
|
||||
* @param options.headerComponent - The dialog header component.
|
||||
* @param options.footerComponent - The dialog footer component.
|
||||
* @param options.component - The dialog component.
|
||||
* @param options.props - The dialog props.
|
||||
* @returns The dialog instance and a function to close the dialog.
|
||||
*/
|
||||
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
|
||||
return {
|
||||
dialog: dialogStore.showExtensionDialog(options),
|
||||
closeDialog: () => dialogStore.closeDialog({ key: options.key })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -394,6 +412,7 @@ export const useDialogService = () => {
|
||||
showSignInDialog,
|
||||
showTopUpCreditsDialog,
|
||||
showUpdatePasswordDialog,
|
||||
showExtensionDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -147,10 +147,33 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
return dialog
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog from a third party extension.
|
||||
* Explicitly keys extension dialogs with `extension-` prefix,
|
||||
* to avoid conflicts & prevent use of internal dialogs (available via `dialogService`).
|
||||
*/
|
||||
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
|
||||
const { key } = options
|
||||
if (!key) {
|
||||
console.error('Extension dialog key is required')
|
||||
return
|
||||
}
|
||||
|
||||
const extKey = key.startsWith('extension-') ? key : `extension-${key}`
|
||||
|
||||
const dialog = dialogStack.value.find((d) => d.key === extKey)
|
||||
if (!dialog) return createDialog({ ...options, key: extKey })
|
||||
|
||||
dialog.visible = true
|
||||
riseDialog(dialog)
|
||||
return dialog
|
||||
}
|
||||
|
||||
return {
|
||||
dialogStack,
|
||||
riseDialog,
|
||||
showDialog,
|
||||
closeDialog
|
||||
closeDialog,
|
||||
showExtensionDialog
|
||||
}
|
||||
})
|
||||
|
||||
@@ -30,7 +30,6 @@ export class ResultItemImpl {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
text?: string
|
||||
|
||||
nodeId: NodeId
|
||||
// 'audio' | 'images' | ...
|
||||
@@ -44,7 +43,6 @@ export class ResultItemImpl {
|
||||
this.filename = obj.filename ?? ''
|
||||
this.subfolder = obj.subfolder ?? ''
|
||||
this.type = obj.type ?? ''
|
||||
this.text = obj.text
|
||||
|
||||
this.nodeId = obj.nodeId
|
||||
this.mediaType = obj.mediaType
|
||||
@@ -195,12 +193,8 @@ export class ResultItemImpl {
|
||||
)
|
||||
}
|
||||
|
||||
get isText(): boolean {
|
||||
return ['text', 'json', 'display_text'].includes(this.mediaType)
|
||||
}
|
||||
|
||||
get supportsPreview(): boolean {
|
||||
return this.isImage || this.isVideo || this.isAudio || this.isText
|
||||
return this.isImage || this.isVideo || this.isAudio
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,21 +233,14 @@ export class TaskItemImpl {
|
||||
}
|
||||
return Object.entries(this.outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as (ResultItem | string)[]).map((item: ResultItem | string) => {
|
||||
if (typeof item === 'string') {
|
||||
return new ResultItemImpl({
|
||||
text: item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
} else {
|
||||
return new ResultItemImpl({
|
||||
(items as ResultItem[]).map(
|
||||
(item: ResultItem) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ComfyWorkflow extends UserFile {
|
||||
/**
|
||||
* Whether the workflow has been modified comparing to the initial state.
|
||||
*/
|
||||
private _isModified: boolean = false
|
||||
_isModified: boolean = false
|
||||
|
||||
/**
|
||||
* @param options The path, modified, and size of the workflow.
|
||||
@@ -131,7 +131,7 @@ export interface WorkflowStore {
|
||||
activeWorkflow: LoadedComfyWorkflow | null
|
||||
isActive: (workflow: ComfyWorkflow) => boolean
|
||||
openWorkflows: ComfyWorkflow[]
|
||||
openedWorkflowIndexShift: (shift: number) => LoadedComfyWorkflow | null
|
||||
openedWorkflowIndexShift: (shift: number) => ComfyWorkflow | null
|
||||
openWorkflow: (workflow: ComfyWorkflow) => Promise<LoadedComfyWorkflow>
|
||||
openWorkflowsInBackground: (paths: {
|
||||
left?: string[]
|
||||
@@ -477,7 +477,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
isSubgraphActive,
|
||||
updateActiveGraph
|
||||
}
|
||||
}) as () => WorkflowStore
|
||||
}) satisfies () => WorkflowStore
|
||||
|
||||
export const useWorkflowBookmarkStore = defineStore('workflowBookmark', () => {
|
||||
const bookmarks = ref<Set<string>>(new Set())
|
||||
|
||||
@@ -100,6 +100,7 @@ export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
colorPalette,
|
||||
dialog,
|
||||
bottomPanel,
|
||||
defineStore,
|
||||
user: partialUserStore,
|
||||
|
||||
registerSidebarTab,
|
||||
|
||||
18
tsconfig.eslint.json
Normal file
18
tsconfig.eslint.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
/* Test files should not be compiled */
|
||||
"noEmit": true,
|
||||
// "strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"*.ts",
|
||||
"*.mts",
|
||||
"*.config.js",
|
||||
"browser_tests/**/*.ts",
|
||||
"tests-ui/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -8,11 +8,7 @@ import { createHtmlPlugin } from 'vite-plugin-html'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import type { UserConfigExport } from 'vitest/config'
|
||||
|
||||
import {
|
||||
addElementVnodeExportPlugin,
|
||||
comfyAPIPlugin,
|
||||
generateImportMapPlugin
|
||||
} from './build/plugins'
|
||||
import { comfyAPIPlugin, generateImportMapPlugin } from './build/plugins'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@@ -77,11 +73,40 @@ export default defineConfig({
|
||||
: [vue()]),
|
||||
comfyAPIPlugin(IS_DEV),
|
||||
generateImportMapPlugin([
|
||||
{ name: 'vue', pattern: /[\\/]node_modules[\\/]vue[\\/]/ },
|
||||
{ name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ },
|
||||
{ name: 'vue-i18n', pattern: /[\\/]node_modules[\\/]vue-i18n[\\/]/ }
|
||||
{
|
||||
name: 'vue',
|
||||
pattern: 'vue',
|
||||
entry: './dist/vue.esm-browser.prod.js'
|
||||
},
|
||||
{
|
||||
name: 'vue-i18n',
|
||||
pattern: 'vue-i18n',
|
||||
entry: './dist/vue-i18n.esm-browser.prod.js'
|
||||
},
|
||||
{
|
||||
name: 'primevue',
|
||||
pattern: /^primevue\/?.*/,
|
||||
entry: './index.mjs',
|
||||
recursiveDependence: true
|
||||
},
|
||||
{
|
||||
name: '@primevue/themes',
|
||||
pattern: /^@primevue\/themes\/?.*/,
|
||||
entry: './index.mjs',
|
||||
recursiveDependence: true
|
||||
},
|
||||
{
|
||||
name: '@primevue/forms',
|
||||
pattern: /^@primevue\/forms\/?.*/,
|
||||
entry: './index.mjs',
|
||||
recursiveDependence: true,
|
||||
override: {
|
||||
'@primeuix/forms': {
|
||||
entry: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
]),
|
||||
addElementVnodeExportPlugin(),
|
||||
|
||||
Icons({
|
||||
compiler: 'vue3'
|
||||
|
||||
Reference in New Issue
Block a user