mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 13:59:28 +00:00
Compare commits
12 Commits
media/view
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b211f931 | ||
|
|
89be4d5c1e | ||
|
|
153f32c990 | ||
|
|
d7119efe54 | ||
|
|
2529cca7ad | ||
|
|
1c95015910 | ||
|
|
339ea644fe | ||
|
|
140be34a21 | ||
|
|
227a038d83 | ||
|
|
e0ca790d5d | ||
|
|
ba0d7e3a76 | ||
|
|
7005c0fb28 |
@@ -330,12 +330,9 @@ test.describe('Workflows sidebar', () => {
|
||||
.getPersistedItem('workflow1.json')
|
||||
.click({ button: 'right' })
|
||||
await comfyPage.contextMenu.clickMenuItem('Duplicate')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*workflow1 (Copy).json'
|
||||
])
|
||||
await expect
|
||||
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
||||
.toEqual(['*Unsaved Workflow.json', '*workflow1 (Copy).json'])
|
||||
})
|
||||
|
||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { statusBadgeVariants } from './statusBadge.variants'
|
||||
import type { StatusBadgeVariants } from './statusBadge.variants'
|
||||
|
||||
const {
|
||||
label,
|
||||
severity = 'default',
|
||||
variant
|
||||
variant,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
label?: string | number
|
||||
severity?: StatusBadgeVariants['severity']
|
||||
variant?: StatusBadgeVariants['variant']
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const badgeClass = computed(() =>
|
||||
statusBadgeVariants({
|
||||
severity,
|
||||
variant: variant ?? (label == null ? 'dot' : 'label')
|
||||
})
|
||||
cn(
|
||||
statusBadgeVariants({
|
||||
severity,
|
||||
variant: variant ?? (label == null ? 'dot' : 'label')
|
||||
}),
|
||||
className
|
||||
)
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
>
|
||||
<template #pipCmd>
|
||||
<code
|
||||
class="px-1 py-0.5 rounded text-xs font-mono bg-comfy-menu-bg text-comfy-input-foreground"
|
||||
class="px-1 py-0.5 rounded-sm text-xs font-mono bg-comfy-menu-bg text-comfy-input-foreground"
|
||||
>pip install -U --pre comfyui-manager</code
|
||||
>
|
||||
</template>
|
||||
<template #flag>
|
||||
<code
|
||||
class="px-1 py-0.5 rounded text-xs font-mono bg-comfy-menu-bg text-comfy-input-foreground"
|
||||
class="px-1 py-0.5 rounded-sm text-xs font-mono bg-comfy-menu-bg text-comfy-input-foreground"
|
||||
>--enable-manager</code
|
||||
>
|
||||
</template>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
v-if="isLoggedIn"
|
||||
:show-arrow="false"
|
||||
compact
|
||||
class="shrink-0 p-1"
|
||||
class="shrink-0 p-1 grid w-10"
|
||||
/>
|
||||
<LoginButton v-else-if="isDesktop" class="p-1" />
|
||||
</div>
|
||||
|
||||
33
src/components/ui/input/Input.stories.ts
Normal file
33
src/components/ui/input/Input.stories.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Input from './Input.vue'
|
||||
|
||||
const meta: Meta<typeof Input> = {
|
||||
title: 'Components/Input',
|
||||
component: Input,
|
||||
tags: ['autodocs'],
|
||||
render: (args) => ({
|
||||
components: { Input },
|
||||
setup: () => ({ args }),
|
||||
template: '<Input v-bind="args" placeholder="Enter text..." />'
|
||||
})
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const WithValue: Story = {
|
||||
args: {
|
||||
modelValue: 'Hello, world!'
|
||||
}
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: (args) => ({
|
||||
components: { Input },
|
||||
setup: () => ({ args }),
|
||||
template: '<Input v-bind="args" placeholder="Disabled input" disabled />'
|
||||
})
|
||||
}
|
||||
32
src/components/ui/input/Input.vue
Normal file
32
src/components/ui/input/Input.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className } = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string | number>()
|
||||
|
||||
const inputRef = useTemplateRef<HTMLInputElement>('inputEl')
|
||||
|
||||
defineExpose({
|
||||
focus: () => inputRef.value?.focus(),
|
||||
select: () => inputRef.value?.select()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
ref="inputEl"
|
||||
v-model="modelValue"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full min-w-0 appearance-none rounded-lg border-none bg-secondary-background px-4 py-2 text-sm text-base-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-default disabled:pointer-events-none disabled:opacity-50',
|
||||
className
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
@@ -16,9 +16,15 @@ import type { FocusCallback } from './tagsInputContext'
|
||||
|
||||
const {
|
||||
disabled = false,
|
||||
alwaysEditing = false,
|
||||
class: className,
|
||||
...restProps
|
||||
} = defineProps<TagsInputRootProps<T> & { class?: HTMLAttributes['class'] }>()
|
||||
} = defineProps<
|
||||
TagsInputRootProps<T> & {
|
||||
class?: HTMLAttributes['class']
|
||||
alwaysEditing?: boolean
|
||||
}
|
||||
>()
|
||||
const emits = defineEmits<TagsInputRootEmits<T>>()
|
||||
|
||||
const isEditing = ref(false)
|
||||
@@ -28,9 +34,10 @@ const focusInput = ref<FocusCallback>()
|
||||
provide(tagsInputFocusKey, (callback: FocusCallback) => {
|
||||
focusInput.value = callback
|
||||
})
|
||||
provide(tagsInputIsEditingKey, isEditing)
|
||||
const isEditingEnabled = computed(() => alwaysEditing || isEditing.value)
|
||||
provide(tagsInputIsEditingKey, isEditingEnabled)
|
||||
|
||||
const internalDisabled = computed(() => disabled || !isEditing.value)
|
||||
const internalDisabled = computed(() => disabled || !isEditingEnabled.value)
|
||||
|
||||
const delegatedProps = computed(() => ({
|
||||
...restProps,
|
||||
@@ -40,7 +47,7 @@ const delegatedProps = computed(() => ({
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
|
||||
async function enableEditing() {
|
||||
if (!disabled && !isEditing.value) {
|
||||
if (!disabled && !alwaysEditing && !isEditing.value) {
|
||||
isEditing.value = true
|
||||
await nextTick()
|
||||
focusInput.value?.()
|
||||
@@ -48,7 +55,9 @@ async function enableEditing() {
|
||||
}
|
||||
|
||||
onClickOutside(rootEl, () => {
|
||||
isEditing.value = false
|
||||
if (!alwaysEditing) {
|
||||
isEditing.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -61,7 +70,7 @@ onClickOutside(rootEl, () => {
|
||||
'group relative flex flex-wrap items-center gap-2 rounded-lg bg-transparent p-2 text-xs text-base-foreground',
|
||||
!internalDisabled &&
|
||||
'hover:bg-modal-card-background-hovered focus-within:bg-modal-card-background-hovered',
|
||||
!disabled && !isEditing && 'cursor-pointer',
|
||||
!disabled && !isEditingEnabled && 'cursor-pointer',
|
||||
className
|
||||
)
|
||||
"
|
||||
@@ -69,7 +78,7 @@ onClickOutside(rootEl, () => {
|
||||
>
|
||||
<slot :is-empty="modelValue.length === 0" />
|
||||
<i
|
||||
v-if="!disabled && !isEditing"
|
||||
v-if="!disabled && !isEditingEnabled"
|
||||
aria-hidden="true"
|
||||
class="icon-[lucide--square-pen] absolute bottom-2 right-2 size-4 text-muted-foreground transition-opacity opacity-0 group-hover:opacity-100"
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@ const modelValue = defineModel<string | number>()
|
||||
v-model="modelValue"
|
||||
:class="
|
||||
cn(
|
||||
'flex min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'flex min-h-16 w-full rounded-lg border-none bg-secondary-background px-3 py-2 text-sm text-base-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border-default disabled:pointer-events-none disabled:opacity-50',
|
||||
className
|
||||
)
|
||||
"
|
||||
|
||||
@@ -77,9 +77,7 @@
|
||||
>
|
||||
{{ contentTitle }}
|
||||
</h2>
|
||||
<div
|
||||
class="min-h-0 flex-1 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
|
||||
>
|
||||
<div :class="contentContainerClass">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</main>
|
||||
@@ -153,15 +151,20 @@ const SIZE_CLASSES = {
|
||||
} as const
|
||||
|
||||
type ModalSize = keyof typeof SIZE_CLASSES
|
||||
type ContentPadding = 'default' | 'compact' | 'none'
|
||||
|
||||
const {
|
||||
contentTitle,
|
||||
rightPanelTitle,
|
||||
size = 'lg'
|
||||
size = 'lg',
|
||||
leftPanelWidth = '14rem',
|
||||
contentPadding = 'default'
|
||||
} = defineProps<{
|
||||
contentTitle: string
|
||||
rightPanelTitle?: string
|
||||
size?: ModalSize
|
||||
leftPanelWidth?: string
|
||||
contentPadding?: ContentPadding
|
||||
}>()
|
||||
|
||||
const sizeClasses = computed(() => SIZE_CLASSES[size])
|
||||
@@ -197,10 +200,18 @@ const showLeftPanel = computed(() => {
|
||||
return shouldShow
|
||||
})
|
||||
|
||||
const contentContainerClass = computed(() =>
|
||||
cn(
|
||||
'flex min-h-0 flex-1 flex-col overflow-y-auto scrollbar-custom',
|
||||
contentPadding === 'default' && 'px-6 pt-0 pb-10',
|
||||
contentPadding === 'compact' && 'px-6 pt-0 pb-2'
|
||||
)
|
||||
)
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
gridTemplateColumns: hasRightPanel.value
|
||||
? `${hasLeftPanel.value && showLeftPanel.value ? '14rem' : '0rem'} 1fr ${isRightPanelOpen.value ? '18rem' : '0rem'}`
|
||||
: `${hasLeftPanel.value && showLeftPanel.value ? '14rem' : '0rem'} 1fr`
|
||||
? `${hasLeftPanel.value && showLeftPanel.value ? leftPanelWidth : '0rem'} 1fr ${isRightPanelOpen.value ? '18rem' : '0rem'}`
|
||||
: `${hasLeftPanel.value && showLeftPanel.value ? leftPanelWidth : '0rem'} 1fr`
|
||||
}))
|
||||
|
||||
const toggleLeftPanel = () => {
|
||||
|
||||
@@ -183,6 +183,31 @@ describe('useWorkflowStore', () => {
|
||||
const workflow = store.createTemporary('a.json')
|
||||
expect(workflow.path).toBe('workflows/a (2).json')
|
||||
})
|
||||
|
||||
it('should assign a workflow id to newly created temporary workflows', () => {
|
||||
const workflow = store.createTemporary('id-test.json')
|
||||
const state = JSON.parse(workflow.content!)
|
||||
|
||||
expect(typeof state.id).toBe('string')
|
||||
expect(state.id.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should assign an id when temporary workflow data is missing one', () => {
|
||||
const workflowDataWithoutId = {
|
||||
...defaultGraph,
|
||||
id: undefined
|
||||
}
|
||||
|
||||
const workflow = store.createTemporary(
|
||||
'missing-id.json',
|
||||
workflowDataWithoutId
|
||||
)
|
||||
const state = JSON.parse(workflow.content!)
|
||||
|
||||
expect(typeof state.id).toBe('string')
|
||||
expect(state.id.length).toBeGreaterThan(0)
|
||||
expect(workflowDataWithoutId.id).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('openWorkflow', () => {
|
||||
|
||||
@@ -255,6 +255,20 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
return workflow
|
||||
}
|
||||
|
||||
const ensureWorkflowId = (
|
||||
workflowData?: ComfyWorkflowJSON
|
||||
): ComfyWorkflowJSON => {
|
||||
const base = workflowData
|
||||
? (JSON.parse(JSON.stringify(workflowData)) as ComfyWorkflowJSON)
|
||||
: (JSON.parse(defaultGraphJSON) as ComfyWorkflowJSON)
|
||||
|
||||
if (!base.id) {
|
||||
base.id = generateUUID()
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a new temporary workflow
|
||||
*/
|
||||
@@ -268,9 +282,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
size: -1
|
||||
})
|
||||
|
||||
workflow.originalContent = workflow.content = workflowData
|
||||
? JSON.stringify(workflowData)
|
||||
: defaultGraphJSON
|
||||
const initialWorkflowData = ensureWorkflowId(workflowData)
|
||||
workflow.originalContent = workflow.content =
|
||||
JSON.stringify(initialWorkflowData)
|
||||
|
||||
workflowLookup.value[workflow.path] = workflow
|
||||
return workflow
|
||||
@@ -284,9 +298,13 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
ComfyWorkflow.basePath + (path ?? 'Unsaved Workflow.json')
|
||||
)
|
||||
|
||||
const normalizedWorkflowData = workflowData
|
||||
? ensureWorkflowId(workflowData)
|
||||
: undefined
|
||||
|
||||
// Try to reuse an existing loaded workflow with the same filename
|
||||
// that is not stored in the workflows directory
|
||||
if (path && workflowData) {
|
||||
if (path && normalizedWorkflowData) {
|
||||
const existingWorkflow = workflows.value.find(
|
||||
(w) => w.fullFilename === path
|
||||
)
|
||||
@@ -296,12 +314,12 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
ComfyWorkflow.basePath.slice(0, -1)
|
||||
)
|
||||
) {
|
||||
existingWorkflow.changeTracker.reset(workflowData)
|
||||
existingWorkflow.changeTracker.reset(normalizedWorkflowData)
|
||||
return existingWorkflow
|
||||
}
|
||||
}
|
||||
|
||||
return createNewWorkflow(fullPath, workflowData)
|
||||
return createNewWorkflow(fullPath, normalizedWorkflowData)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,19 @@ import { getPathDetails } from '@/utils/formatUtil'
|
||||
import { syncEntities } from '@/utils/syncUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
/**
|
||||
* Normalizes a timestamp value that may be either a number (milliseconds)
|
||||
* or an ISO 8601 string (from Go's time.Time JSON serialization) into
|
||||
* a consistent millisecond timestamp.
|
||||
*/
|
||||
function normalizeTimestamp(value: number | string): number {
|
||||
if (typeof value === 'string') {
|
||||
const ms = new Date(value).getTime()
|
||||
return Number.isNaN(ms) ? Date.now() : ms
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file in the user's data directory.
|
||||
*/
|
||||
@@ -140,7 +153,7 @@ export class UserFile {
|
||||
// https://github.com/comfyanonymous/ComfyUI/pull/5446
|
||||
const updatedFile = (await resp.json()) as string | UserDataFullInfo
|
||||
if (typeof updatedFile === 'object') {
|
||||
this.lastModified = updatedFile.modified
|
||||
this.lastModified = normalizeTimestamp(updatedFile.modified)
|
||||
this.size = updatedFile.size
|
||||
}
|
||||
this.originalContent = this.content
|
||||
@@ -175,7 +188,7 @@ export class UserFile {
|
||||
// https://github.com/comfyanonymous/ComfyUI/pull/5446
|
||||
const updatedFile = (await resp.json()) as string | UserDataFullInfo
|
||||
if (typeof updatedFile === 'object') {
|
||||
this.lastModified = updatedFile.modified
|
||||
this.lastModified = normalizeTimestamp(updatedFile.modified)
|
||||
this.size = updatedFile.size
|
||||
}
|
||||
return this
|
||||
|
||||
Reference in New Issue
Block a user