mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
feat/fix: App mode QA updates (#9439)
## Summary Various fixes from app mode QA ## Changes - **What**: - fix: prevent inserting nodes from workflow/apps sidebar tabs - fix: hide json extension in workflow tab - fix: hide apps nav button in apps tab when already in apps mode - fix: center text on arrange page - fix: prevent IoItems from "jumping" due to stale transform after drag and drop op - fix: refactor side panels and add custom stable pixel based sizing - fix: make outputs/inputs lists in app builder scrollable - fix: fix rerun not working correctly - feat: add text to interrupt button - feat: add enter app mode button to builder toolbar - feat: add tooltip to download button on linear view - feat: show last output of workflow in arrange tab if available - feat: show download count in download all button, hide if only 1 asset to download ## Review Focus - Rerun - I am not sure why it was triggering widget actions, removing it seemed like the correct fix - useStablePrimeVueSplitter - this is a workaround for the fact it uses percent sizing, I also tried switching to reka-ui splitters, but they also only support % sizing in our version [pixel based looks to have been added in a newer version, will log an issue to upgrade & replace splitters with this] ## Screenshots (if applicable) <img width="1314" height="1129" alt="image" src="https://github.com/user-attachments/assets/c430f9d6-7c29-4853-803e-5b6fe7086fca" /> <img width="511" height="283" alt="image" src="https://github.com/user-attachments/assets/b7e594d4-70a1-41e3-8ba1-78512f2a5c8b" /> <img width="254" height="232" alt="image" src="https://github.com/user-attachments/assets/1d146399-39ea-4b0e-928c-340b74957535" /> <img width="487" height="198" alt="image" src="https://github.com/user-attachments/assets/e2ba7f5d-8ff5-47f4-9526-61ebb99514b8" /> <img width="378" height="647" alt="image" src="https://github.com/user-attachments/assets/a47a3054-9320-4327-bdc0-b0a16e19f83d" /> <img width="1016" height="476" alt="image" src="https://github.com/user-attachments/assets/479ae50e-d380-4d56-a5c9-5df142b14ed0" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9439-feat-fix-App-mode-QA-updates-31a6d73d365081b38337d63207b88817) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
import { ref, useTemplateRef } from 'vue'
|
||||
|
||||
import ZoomPane from '@/components/ui/ZoomPane.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const { src } = defineProps<{
|
||||
src: string
|
||||
@@ -13,7 +16,11 @@ const width = ref('')
|
||||
const height = ref('')
|
||||
</script>
|
||||
<template>
|
||||
<ZoomPane v-if="!mobile" v-slot="slotProps" class="w-full flex-1">
|
||||
<ZoomPane
|
||||
v-if="!mobile"
|
||||
v-slot="slotProps"
|
||||
:class="cn('w-full flex-1', $attrs.class as string)"
|
||||
>
|
||||
<img
|
||||
ref="imageRef"
|
||||
:src
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { flattenNodeOutput } from '@/renderer/extensions/linearMode/flattenNodeOutput'
|
||||
import MediaOutputPreview from '@/renderer/extensions/linearMode/MediaOutputPreview.vue'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { setMode } = useAppMode()
|
||||
const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
const appModeStore = useAppModeStore()
|
||||
const { hasOutputs } = storeToRefs(appModeStore)
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const { nodeIdToNodeLocatorId } = useWorkflowStore()
|
||||
|
||||
const existingOutput = computed(() => {
|
||||
for (const nodeId of appModeStore.selectedOutputs) {
|
||||
const locatorId = nodeIdToNodeLocatorId(nodeId)
|
||||
const nodeOutput = nodeOutputStore.nodeOutputs[locatorId]
|
||||
if (!nodeOutput) continue
|
||||
const results = flattenNodeOutput([nodeId, nodeOutput])
|
||||
if (results.length > 0) return results[0]
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MediaOutputPreview
|
||||
v-if="existingOutput"
|
||||
:output="existingOutput"
|
||||
class="px-12 py-24"
|
||||
/>
|
||||
<div
|
||||
v-if="hasOutputs"
|
||||
v-else-if="hasOutputs"
|
||||
role="article"
|
||||
data-testid="arrange-preview"
|
||||
class="mx-auto flex h-full w-3/4 flex-col items-center justify-center gap-6 p-8"
|
||||
@@ -23,7 +48,7 @@ const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
<p class="mb-0 font-bold text-base-foreground">
|
||||
{{ t('linearMode.arrange.outputs') }}
|
||||
</p>
|
||||
<p>{{ t('linearMode.arrange.resultsLabel') }}</p>
|
||||
<p class="text-center">{{ t('linearMode.arrange.resultsLabel') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
@@ -15,23 +15,15 @@ import LatentPreview from '@/renderer/extensions/linearMode/LatentPreview.vue'
|
||||
import LinearWelcome from '@/renderer/extensions/linearMode/LinearWelcome.vue'
|
||||
import LinearArrange from '@/renderer/extensions/linearMode/LinearArrange.vue'
|
||||
import LinearFeedback from '@/renderer/extensions/linearMode/LinearFeedback.vue'
|
||||
import MediaOutputPreview from '@/renderer/extensions/linearMode/MediaOutputPreview.vue'
|
||||
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
|
||||
import { useOutputHistory } from '@/renderer/extensions/linearMode/useOutputHistory'
|
||||
import type { OutputSelection } from '@/renderer/extensions/linearMode/linearModeTypes'
|
||||
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
|
||||
import { getMediaType } from '@/renderer/extensions/linearMode/mediaTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
import { executeWidgetsCallback } from '@/utils/litegraphUtil'
|
||||
|
||||
// Lazy-loaded to avoid pulling THREE.js into the main bundle
|
||||
const Preview3d = defineAsyncComponent(
|
||||
() => import('@/renderer/extensions/linearMode/Preview3d.vue')
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
@@ -73,17 +65,12 @@ async function loadWorkflow(item: AssetItem | undefined) {
|
||||
const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker
|
||||
if (!changeTracker) return app.loadGraphData(workflow)
|
||||
changeTracker.redoQueue = []
|
||||
changeTracker.updateState([workflow], changeTracker.undoQueue)
|
||||
await changeTracker.updateState([workflow], changeTracker.undoQueue)
|
||||
}
|
||||
|
||||
async function rerun(e: Event) {
|
||||
if (!runButtonClick) return
|
||||
await loadWorkflow(selectedItem.value)
|
||||
//FIXME don't use timeouts here
|
||||
//Currently seeds fail to properly update even with timeouts?
|
||||
await new Promise((r) => setTimeout(r, 500))
|
||||
executeWidgetsCallback(collectAllNodes(app.rootGraph), 'afterQueued')
|
||||
|
||||
runButtonClick(e)
|
||||
}
|
||||
</script>
|
||||
@@ -106,6 +93,7 @@ async function rerun(e: Event) {
|
||||
</template>
|
||||
<Button
|
||||
v-if="selectedOutput"
|
||||
v-tooltip.top="t('g.download')"
|
||||
size="icon"
|
||||
:aria-label="t('g.download')"
|
||||
@click="
|
||||
@@ -119,21 +107,26 @@ async function rerun(e: Event) {
|
||||
<Button
|
||||
v-if="!executionStore.isIdle && !selectedItem"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="commandStore.execute('Comfy.Interrupt')"
|
||||
>
|
||||
<i class="icon-[lucide--x]" />
|
||||
{{ t('linearMode.cancelThisRun') }}
|
||||
</Button>
|
||||
<Popover
|
||||
v-if="selectedItem"
|
||||
:entries="[
|
||||
{
|
||||
icon: 'icon-[lucide--download]',
|
||||
label: t('linearMode.downloadAll'),
|
||||
command: () => downloadAsset(selectedItem)
|
||||
},
|
||||
{ separator: true },
|
||||
...(allOutputs(selectedItem).length > 1
|
||||
? [
|
||||
{
|
||||
icon: 'icon-[lucide--download]',
|
||||
label: t('linearMode.downloadAll', {
|
||||
count: allOutputs(selectedItem).length
|
||||
}),
|
||||
command: () => downloadAsset(selectedItem)
|
||||
},
|
||||
{ separator: true }
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: 'icon-[lucide--trash-2]',
|
||||
label: t('queue.jobMenu.deleteAsset'),
|
||||
@@ -143,32 +136,14 @@ async function rerun(e: Event) {
|
||||
/>
|
||||
</section>
|
||||
<ImagePreview
|
||||
v-if="
|
||||
(canShowPreview && latentPreview) ||
|
||||
getMediaType(selectedOutput) === 'images'
|
||||
"
|
||||
v-if="canShowPreview && latentPreview"
|
||||
:mobile
|
||||
:src="(canShowPreview && latentPreview) || selectedOutput!.url"
|
||||
:src="latentPreview"
|
||||
/>
|
||||
<VideoPreview
|
||||
v-else-if="getMediaType(selectedOutput) === 'video'"
|
||||
:src="selectedOutput!.url"
|
||||
class="flex-1 object-contain md:p-3 md:contain-size"
|
||||
/>
|
||||
<audio
|
||||
v-else-if="getMediaType(selectedOutput) === 'audio'"
|
||||
class="m-auto w-full"
|
||||
controls
|
||||
:src="selectedOutput!.url"
|
||||
/>
|
||||
<article
|
||||
v-else-if="getMediaType(selectedOutput) === 'text'"
|
||||
class="m-auto my-12 w-full max-w-lg overflow-y-auto"
|
||||
v-text="selectedOutput!.url"
|
||||
/>
|
||||
<Preview3d
|
||||
v-else-if="getMediaType(selectedOutput) === '3d'"
|
||||
:model-url="selectedOutput!.url"
|
||||
<MediaOutputPreview
|
||||
v-else-if="selectedOutput"
|
||||
:output="selectedOutput"
|
||||
:mobile
|
||||
/>
|
||||
<LatentPreview v-else-if="queueStore.runningTasks.length > 0" />
|
||||
<LinearArrange v-else-if="isArrangeMode" />
|
||||
|
||||
55
src/renderer/extensions/linearMode/MediaOutputPreview.vue
Normal file
55
src/renderer/extensions/linearMode/MediaOutputPreview.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, useAttrs } from 'vue'
|
||||
|
||||
import ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'
|
||||
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
|
||||
import { getMediaType } from '@/renderer/extensions/linearMode/mediaTypes'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const Preview3d = defineAsyncComponent(
|
||||
() => import('@/renderer/extensions/linearMode/Preview3d.vue')
|
||||
)
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const { output } = defineProps<{
|
||||
output: ResultItemImpl
|
||||
mobile?: boolean
|
||||
}>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
</script>
|
||||
<template>
|
||||
<ImagePreview
|
||||
v-if="getMediaType(output) === 'images'"
|
||||
:class="attrs.class as string"
|
||||
:mobile
|
||||
:src="output.url"
|
||||
/>
|
||||
<VideoPreview
|
||||
v-else-if="getMediaType(output) === 'video'"
|
||||
:src="output.url"
|
||||
:class="
|
||||
cn('flex-1 object-contain md:p-3 md:contain-size', attrs.class as string)
|
||||
"
|
||||
/>
|
||||
<audio
|
||||
v-else-if="getMediaType(output) === 'audio'"
|
||||
:class="cn('m-auto w-full', attrs.class as string)"
|
||||
controls
|
||||
:src="output.url"
|
||||
/>
|
||||
<article
|
||||
v-else-if="getMediaType(output) === 'text'"
|
||||
:class="
|
||||
cn('m-auto my-12 w-full max-w-lg overflow-y-auto', attrs.class as string)
|
||||
"
|
||||
v-text="output.url"
|
||||
/>
|
||||
<Preview3d
|
||||
v-else-if="getMediaType(output) === '3d'"
|
||||
:class="attrs.class as string"
|
||||
:model-url="output.url"
|
||||
/>
|
||||
</template>
|
||||
Reference in New Issue
Block a user