mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
## Summary Additional updates ## Changes - **What**: - Share widget rename functionality with properties panel implementation - Add hammer icon to builder mode tabs - Change (!) to (i) on app builder info sections ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9545-feat-fix-App-mode-further-updates-31c6d73d36508104aaa9c5f1e6205a0b) by [Unito](https://www.unito.io)
298 lines
8.4 KiB
Vue
298 lines
8.4 KiB
Vue
<template>
|
|
<ContextMenuRoot>
|
|
<ContextMenuTrigger as-child>
|
|
<div
|
|
ref="workflowTabRef"
|
|
class="workflow-tab group flex gap-2 p-2"
|
|
v-bind="$attrs"
|
|
@mouseenter="handleMouseEnter"
|
|
@mouseleave="handleMouseLeave"
|
|
@mouseup="handleMouseUp"
|
|
@click="handleClick"
|
|
>
|
|
<i v-if="isBuilderState" class="bg-text-subtle icon-[lucide--hammer]" />
|
|
<i
|
|
v-else-if="workflowOption.workflow.initialMode === 'app'"
|
|
class="icon-[lucide--panels-top-left] bg-primary-background"
|
|
/>
|
|
<span
|
|
class="workflow-label inline-block max-w-[150px] truncate text-sm"
|
|
>
|
|
{{ workflowOption.workflow.filename }}
|
|
</span>
|
|
<div class="relative">
|
|
<span
|
|
v-if="shouldShowStatusIndicator"
|
|
class="absolute top-1/2 left-1/2 z-10 w-4 -translate-1/2 bg-(--comfy-menu-bg) text-2xl font-bold group-hover:hidden"
|
|
>•</span
|
|
>
|
|
<Button
|
|
class="close-button invisible w-auto p-0"
|
|
variant="muted-textonly"
|
|
size="icon-sm"
|
|
:aria-label="t('g.close')"
|
|
@click.stop="onCloseWorkflow(workflowOption)"
|
|
>
|
|
<i class="pi pi-times" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuPortal>
|
|
<ContextMenuContent
|
|
class="z-1000 min-w-56 rounded-lg border border-border-subtle bg-base-background px-2 py-3 shadow-interface"
|
|
>
|
|
<WorkflowActionsList
|
|
:items="contextMenuItems"
|
|
:item-component="ContextMenuItem"
|
|
:separator-component="ContextMenuSeparator"
|
|
/>
|
|
</ContextMenuContent>
|
|
</ContextMenuPortal>
|
|
</ContextMenuRoot>
|
|
|
|
<WorkflowTabPopover
|
|
ref="popoverRef"
|
|
:workflow-filename="workflowOption.workflow.filename"
|
|
:thumbnail-url="thumbnailUrl"
|
|
:is-active-tab="isActiveTab"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuPortal,
|
|
ContextMenuRoot,
|
|
ContextMenuSeparator,
|
|
ContextMenuTrigger
|
|
} from 'reka-ui'
|
|
import { computed, onUnmounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import WorkflowActionsList from '@/components/common/WorkflowActionsList.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import {
|
|
usePragmaticDraggable,
|
|
usePragmaticDroppable
|
|
} from '@/composables/usePragmaticDragAndDrop'
|
|
import { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
|
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
|
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
|
import type { WorkflowMenuItem } from '@/types/workflowMenuItem'
|
|
|
|
import WorkflowTabPopover from './WorkflowTabPopover.vue'
|
|
|
|
interface WorkflowOption {
|
|
value: string
|
|
workflow: ComfyWorkflow
|
|
}
|
|
|
|
const props = defineProps<{
|
|
workflowOption: WorkflowOption
|
|
isFirst: boolean
|
|
isLast: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
closeToLeft: []
|
|
closeToRight: []
|
|
closeOthers: []
|
|
mouseup: [event: MouseEvent]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const workspaceStore = useWorkspaceStore()
|
|
const workflowStore = useWorkflowStore()
|
|
const settingStore = useSettingStore()
|
|
const workflowTabRef = ref<HTMLElement | null>(null)
|
|
const popoverRef = ref<InstanceType<typeof WorkflowTabPopover> | null>(null)
|
|
const workflowThumbnail = useWorkflowThumbnail()
|
|
|
|
// Use computed refs to cache autosave settings
|
|
const autoSaveSetting = computed(() =>
|
|
settingStore.get('Comfy.Workflow.AutoSave')
|
|
)
|
|
const autoSaveDelay = computed(() =>
|
|
settingStore.get('Comfy.Workflow.AutoSaveDelay')
|
|
)
|
|
|
|
const shouldShowStatusIndicator = computed(() => {
|
|
if (workspaceStore.shiftDown) {
|
|
// Branch 1: Shift key is held down, do not show the status indicator.
|
|
return false
|
|
}
|
|
if (!props.workflowOption.workflow.isPersisted) {
|
|
// Branch 2: Workflow is not persisted, show the status indicator.
|
|
return true
|
|
}
|
|
if (props.workflowOption.workflow.isModified) {
|
|
// Branch 3: Workflow is modified.
|
|
if (autoSaveSetting.value === 'off') {
|
|
// Sub-branch 3a: Autosave is off, so show the status indicator.
|
|
return true
|
|
}
|
|
if (autoSaveSetting.value === 'after delay' && autoSaveDelay.value > 3000) {
|
|
// Sub-branch 3b: Autosave delay is too high, so show the status indicator.
|
|
return true
|
|
}
|
|
// Sub-branch 3c: Workflow is modified but no condition applies, do not show the status indicator.
|
|
return false
|
|
}
|
|
// Default: do not show the status indicator. This should not be reachable.
|
|
return false
|
|
})
|
|
|
|
const isBuilderState = computed(() => {
|
|
const currentMode = props.workflowOption.workflow.activeMode
|
|
return typeof currentMode === 'string' && currentMode.startsWith('builder:')
|
|
})
|
|
|
|
const isActiveTab = computed(() => {
|
|
return workflowStore.activeWorkflow?.key === props.workflowOption.workflow.key
|
|
})
|
|
|
|
const thumbnailUrl = computed(() => {
|
|
return workflowThumbnail.getThumbnail(props.workflowOption.workflow.key)
|
|
})
|
|
|
|
// Event handlers that delegate to the popover component
|
|
const handleMouseEnter = (event: Event) => {
|
|
popoverRef.value?.showPopover(event)
|
|
}
|
|
|
|
const handleMouseLeave = () => {
|
|
popoverRef.value?.hidePopover()
|
|
}
|
|
|
|
const handleClick = (event: Event) => {
|
|
popoverRef.value?.togglePopover(event)
|
|
}
|
|
|
|
const handleMouseUp = (event: MouseEvent) => {
|
|
emit('mouseup', event)
|
|
}
|
|
|
|
const closeWorkflows = async (options: WorkflowOption[]) => {
|
|
for (const opt of options) {
|
|
if (
|
|
!(await useWorkflowService().closeWorkflow(opt.workflow, {
|
|
warnIfUnsaved: !workspaceStore.shiftDown,
|
|
hint: t('sideToolbar.workflowTab.dirtyCloseHint')
|
|
}))
|
|
) {
|
|
// User clicked cancel
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
const onCloseWorkflow = async (option: WorkflowOption) => {
|
|
await closeWorkflows([option])
|
|
}
|
|
|
|
const commandStore = useCommandStore()
|
|
const workflow = computed(() => props.workflowOption.workflow)
|
|
|
|
const { menuItems: baseMenuItems } = useWorkflowActionsMenu(
|
|
() => commandStore.execute('Comfy.RenameWorkflow'),
|
|
{ includeDelete: false, workflow }
|
|
)
|
|
|
|
const contextMenuItems = computed<WorkflowMenuItem[]>(() => [
|
|
...baseMenuItems.value,
|
|
{ separator: true },
|
|
{
|
|
id: 'close-tab',
|
|
label: t('tabMenu.closeTab'),
|
|
icon: 'pi pi-times',
|
|
command: () => onCloseWorkflow(props.workflowOption)
|
|
},
|
|
{
|
|
id: 'close-tabs-to-left',
|
|
label: t('tabMenu.closeTabsToLeft'),
|
|
overlayIcon: {
|
|
mainIcon: 'pi pi-times',
|
|
subIcon: 'pi pi-arrow-left',
|
|
positionX: 'right',
|
|
positionY: 'bottom',
|
|
subIconScale: 0.5
|
|
},
|
|
command: () => emit('closeToLeft'),
|
|
disabled: props.isFirst
|
|
},
|
|
{
|
|
id: 'close-tabs-to-right',
|
|
label: t('tabMenu.closeTabsToRight'),
|
|
overlayIcon: {
|
|
mainIcon: 'pi pi-times',
|
|
subIcon: 'pi pi-arrow-right',
|
|
positionX: 'right',
|
|
positionY: 'bottom',
|
|
subIconScale: 0.5
|
|
},
|
|
command: () => emit('closeToRight'),
|
|
disabled: props.isLast
|
|
},
|
|
{
|
|
id: 'close-other-tabs',
|
|
label: t('tabMenu.closeOtherTabs'),
|
|
overlayIcon: {
|
|
mainIcon: 'pi pi-times',
|
|
subIcon: 'pi pi-arrows-h',
|
|
positionX: 'right',
|
|
positionY: 'bottom',
|
|
subIconScale: 0.5
|
|
},
|
|
command: () => emit('closeOthers'),
|
|
disabled: props.isFirst && props.isLast
|
|
}
|
|
])
|
|
|
|
const tabGetter = () => workflowTabRef.value as HTMLElement
|
|
|
|
usePragmaticDraggable(tabGetter, {
|
|
getInitialData: () => {
|
|
return {
|
|
workflowKey: props.workflowOption.workflow.key
|
|
}
|
|
}
|
|
})
|
|
|
|
usePragmaticDroppable(tabGetter, {
|
|
getData: () => {
|
|
return {
|
|
workflowKey: props.workflowOption.workflow.key
|
|
}
|
|
},
|
|
onDrop: (e) => {
|
|
const fromIndex = workflowStore.openWorkflows.findIndex(
|
|
(wf) => wf.key === e.source.data.workflowKey
|
|
)
|
|
const toIndex = workflowStore.openWorkflows.findIndex(
|
|
(wf) => wf.key === e.location.current.dropTargets[0]?.data.workflowKey
|
|
)
|
|
if (fromIndex !== toIndex) {
|
|
workflowStore.reorderWorkflows(fromIndex, toIndex)
|
|
}
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
popoverRef.value?.hidePopover()
|
|
})
|
|
</script>
|
|
|
|
<style>
|
|
.p-tooltip.workflow-tab-tooltip {
|
|
z-index: 1200 !important;
|
|
}
|
|
</style>
|