mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-09 08:38:13 +00:00
Compare commits
14 Commits
drjkl/cart
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5679af17b1 | ||
|
|
f3af3c90a7 | ||
|
|
63c36d3f2f | ||
|
|
892a9cf2c5 | ||
|
|
308c22efc6 | ||
|
|
5728d240da | ||
|
|
acf2f4280c | ||
|
|
7ad6994d01 | ||
|
|
2829f78579 | ||
|
|
c4156d7059 | ||
|
|
725a0a2b89 | ||
|
|
8a5bcde168 | ||
|
|
83ffaf30c8 | ||
|
|
2875f897dc |
@@ -4,7 +4,7 @@
|
||||
<template v-if="filter.tasks.length === 0">
|
||||
<!-- Empty filter -->
|
||||
<Divider />
|
||||
<p class="text-neutral-400 w-full text-center">
|
||||
<p class="w-full text-center text-neutral-400">
|
||||
{{ $t('maintenance.allOk') }}
|
||||
</p>
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Display: Cards -->
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
|
||||
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
|
||||
<TaskCard
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
@@ -45,7 +45,8 @@ import { useConfirm, useToast } from 'primevue'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type {
|
||||
MaintenanceFilter,
|
||||
@@ -55,6 +56,7 @@ import type {
|
||||
import TaskCard from './TaskCard.vue'
|
||||
import TaskListItem from './TaskListItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
@@ -80,8 +82,7 @@ const executeTask = async (task: MaintenanceTask) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('maintenance.error.toastTitle'),
|
||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
||||
life: 10_000
|
||||
detail: message ?? t('maintenance.error.defaultDescription')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -189,8 +189,7 @@ const completeValidation = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('maintenance.error.cannotContinue'),
|
||||
life: 5_000
|
||||
detail: t('maintenance.error.cannotContinue')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark hide-language-selector>
|
||||
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
|
||||
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
|
||||
<div
|
||||
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
|
||||
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
|
||||
>
|
||||
<h2 class="text-3xl font-semibold text-neutral-100">
|
||||
{{ $t('install.helpImprove') }}
|
||||
@@ -15,7 +15,7 @@
|
||||
<a
|
||||
href="https://comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-400 hover:text-blue-300 underline"
|
||||
class="text-blue-400 underline hover:text-blue-300"
|
||||
>
|
||||
{{ $t('install.privacyPolicy') }} </a
|
||||
>.
|
||||
@@ -33,7 +33,7 @@
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex pt-6 justify-end">
|
||||
<div class="flex justify-end pt-6">
|
||||
<Button
|
||||
:label="$t('g.ok')"
|
||||
icon="pi pi-check"
|
||||
@@ -72,8 +72,7 @@ const updateConsent = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('install.settings.errorUpdatingConsent'),
|
||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
||||
life: 3000
|
||||
detail: t('install.settings.errorUpdatingConsentDetail')
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
|
||||
@@ -46,6 +46,9 @@ const config: KnipConfig = {
|
||||
'.github/workflows/ci-oss-assets-validation.yaml',
|
||||
// Pending integration in stacked PR
|
||||
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
|
||||
// Pending integration - accessible color picker components
|
||||
'src/components/ui/color-picker/ColorPickerSaturationValue.vue',
|
||||
'src/components/ui/color-picker/ColorPickerSlider.vue',
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js'
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.2",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -10,7 +10,6 @@ import PropertiesAccordionItem from '@/components/rightSidePanel/layout/Properti
|
||||
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
@@ -25,9 +24,9 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { DOMWidgetImpl } from '@/scripts/domWidget'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { promptRenameWidget } from '@/utils/widgetUtil'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
||||
@@ -73,15 +72,12 @@ const inputsWithState = computed(() =>
|
||||
}
|
||||
}
|
||||
|
||||
const input = node.inputs.find((i) => i.widget?.name === widget.name)
|
||||
const rename = input && (() => renameWidget(widget, input))
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
widgetName,
|
||||
label: widget.label,
|
||||
subLabel: node.title,
|
||||
rename
|
||||
rename: () => promptRenameWidget(widget, node, t)
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -92,20 +88,6 @@ const outputsWithState = computed<[NodeId, string][]>(() =>
|
||||
])
|
||||
)
|
||||
|
||||
async function renameWidget(widget: IBaseWidget, input: INodeInputSlot) {
|
||||
const newLabel = await useDialogService().prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('g.enterNewNamePrompt'),
|
||||
defaultValue: widget.label,
|
||||
placeholder: widget.name
|
||||
})
|
||||
if (newLabel === null) return
|
||||
widget.label = newLabel || undefined
|
||||
input.label = newLabel || undefined
|
||||
widget.callback?.(widget.value)
|
||||
useCanvasStore().canvas?.setDirty(true)
|
||||
}
|
||||
|
||||
function getHovered(
|
||||
e: MouseEvent
|
||||
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
|
||||
@@ -162,7 +144,11 @@ function handleDown(e: MouseEvent) {
|
||||
}
|
||||
function handleClick(e: MouseEvent) {
|
||||
const [node, widget] = getHovered(e) ?? []
|
||||
if (node?.mode !== LGraphEventMode.ALWAYS)
|
||||
if (
|
||||
node?.mode !== LGraphEventMode.ALWAYS ||
|
||||
!nodeTypeValidForApp(node.type) ||
|
||||
node.has_errors
|
||||
)
|
||||
return canvasInteractions.forwardEventToCanvas(e)
|
||||
|
||||
if (!widget) {
|
||||
@@ -198,7 +184,9 @@ const renderedOutputs = computed(() => {
|
||||
return canvas
|
||||
.graph!.nodes.filter(
|
||||
(n) =>
|
||||
n.constructor.nodeData?.output_node && n.mode === LGraphEventMode.ALWAYS
|
||||
n.constructor.nodeData?.output_node &&
|
||||
n.mode === LGraphEventMode.ALWAYS &&
|
||||
!n.has_errors
|
||||
)
|
||||
.map(nodeToDisplayTuple)
|
||||
})
|
||||
@@ -260,7 +248,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.inputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
<i class="icon-[lucide--info] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
@@ -315,7 +303,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.outputs') }}
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
<i class="icon-[lucide--info] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
|
||||
@@ -138,8 +138,7 @@ onMounted(async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToFetchLogs'),
|
||||
life: 5000
|
||||
detail: t('toastMessages.failedToFetchLogs')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -275,8 +275,7 @@ async function handleBuy() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('credits.topUp.purchaseError'),
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
|
||||
life: 5000
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -98,8 +98,7 @@ async function onConfirmCancel() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('subscription.cancelDialog.failed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
|
||||
@@ -579,8 +579,7 @@ const onUpdateComfyUI = async (): Promise<void> => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error.value || t('helpCenter.updateComfyUIFailed'),
|
||||
life: 5000
|
||||
detail: error.value || t('helpCenter.updateComfyUIFailed')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -597,8 +596,7 @@ const onUpdateComfyUI = async (): Promise<void> => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: err instanceof Error ? err.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: err instanceof Error ? err.message : t('g.unknownError')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
/>
|
||||
</div>
|
||||
<SliderControl
|
||||
v-model="brushSize"
|
||||
v-model="brushSizeSliderValue"
|
||||
class="flex-1"
|
||||
label=""
|
||||
:min="1"
|
||||
:max="250"
|
||||
:step="1"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,6 +182,26 @@ const brushSize = computed({
|
||||
set: (value: number) => store.setBrushSize(value)
|
||||
})
|
||||
|
||||
const rawSliderValue = ref<number | null>(null)
|
||||
|
||||
const brushSizeSliderValue = computed({
|
||||
get: () => {
|
||||
if (rawSliderValue.value !== null) {
|
||||
const cachedSize = Math.round(Math.pow(250, rawSliderValue.value))
|
||||
if (cachedSize === brushSize.value) {
|
||||
return rawSliderValue.value
|
||||
}
|
||||
}
|
||||
|
||||
return Math.log(brushSize.value) / Math.log(250)
|
||||
},
|
||||
set: (value: number) => {
|
||||
rawSliderValue.value = value
|
||||
const size = Math.round(Math.pow(250, value))
|
||||
store.setBrushSize(size)
|
||||
}
|
||||
})
|
||||
|
||||
const brushOpacity = computed({
|
||||
get: () => store.brushSettings.opacity,
|
||||
set: (value: number) => store.setBrushOpacity(value)
|
||||
|
||||
@@ -15,10 +15,9 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
|
||||
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
|
||||
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||
|
||||
const {
|
||||
@@ -42,7 +41,6 @@ const label = defineModel<string>('label', { required: true })
|
||||
const canvasStore = useCanvasStore()
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const dialogService = useDialogService()
|
||||
const { t } = useI18n()
|
||||
|
||||
const hasParents = computed(() => parents?.length > 0)
|
||||
@@ -67,15 +65,8 @@ const isCurrentValueDefault = computed(() => {
|
||||
})
|
||||
|
||||
async function handleRename() {
|
||||
const newLabel = await dialogService.prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('g.enterNewNamePrompt'),
|
||||
defaultValue: widget.label,
|
||||
placeholder: widget.name
|
||||
})
|
||||
|
||||
if (newLabel === null) return
|
||||
label.value = newLabel
|
||||
const newLabel = await promptWidgetLabel(widget, t)
|
||||
if (newLabel !== null) label.value = newLabel
|
||||
}
|
||||
|
||||
function handleHideInput() {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
tabindex="0"
|
||||
:aria-label="
|
||||
t('assetBrowser.ariaLabel.assetCard', {
|
||||
name: item.asset.name,
|
||||
name: getAssetDisplayName(item.asset),
|
||||
type: getAssetMediaType(item.asset)
|
||||
})
|
||||
"
|
||||
@@ -44,7 +44,7 @@
|
||||
)
|
||||
"
|
||||
:preview-url="getAssetPreviewUrl(item.asset)"
|
||||
:preview-alt="item.asset.name"
|
||||
:preview-alt="getAssetDisplayName(item.asset)"
|
||||
:icon-name="iconForMediaType(getAssetMediaType(item.asset))"
|
||||
:is-video-preview="isVideoAsset(item.asset)"
|
||||
:primary-text="getAssetPrimaryText(item.asset)"
|
||||
@@ -133,8 +133,12 @@ const listGridStyle = {
|
||||
gap: '0.5rem'
|
||||
}
|
||||
|
||||
function getAssetDisplayName(asset: AssetItem): string {
|
||||
return asset.display_name || asset.name
|
||||
}
|
||||
|
||||
function getAssetPrimaryText(asset: AssetItem): string {
|
||||
return truncateFilename(asset.name)
|
||||
return truncateFilename(getAssetDisplayName(asset))
|
||||
}
|
||||
|
||||
function getAssetMediaType(asset: AssetItem) {
|
||||
|
||||
@@ -569,7 +569,7 @@ const handleZoomClick = (asset: AssetItem) => {
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.showDialog({
|
||||
key: 'asset-3d-viewer',
|
||||
title: asset.name,
|
||||
title: asset.display_name || asset.name,
|
||||
component: Load3dViewerContent,
|
||||
props: {
|
||||
modelUrl: asset.preview_url || ''
|
||||
@@ -615,8 +615,7 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('sideToolbar.folderView.errorSummary'),
|
||||
detail: t('sideToolbar.folderView.errorDetail'),
|
||||
life: 5000
|
||||
detail: t('sideToolbar.folderView.errorDetail')
|
||||
})
|
||||
exitFolderView()
|
||||
}
|
||||
@@ -662,8 +661,7 @@ const copyJobId = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('mediaAsset.jobIdToast.error'),
|
||||
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
@mouseup="handleMouseUp"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i v-if="isBuilderState" class="bg-text-subtle icon-[lucide--hammer]" />
|
||||
<i
|
||||
v-if="workflowOption.workflow.initialMode === 'app'"
|
||||
v-else-if="workflowOption.workflow.initialMode === 'app'"
|
||||
class="icon-[lucide--panels-top-left] bg-primary-background"
|
||||
/>
|
||||
<span
|
||||
@@ -149,6 +150,11 @@ const shouldShowStatusIndicator = computed(() => {
|
||||
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
|
||||
})
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { hue } = defineProps<{
|
||||
hue: number
|
||||
}>()
|
||||
|
||||
const saturation = defineModel<number>('saturation', { required: true })
|
||||
const value = defineModel<number>('value', { required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const hueBackground = computed(() => `hsl(${hue}, 100%, 50%)`)
|
||||
|
||||
const handleStyle = computed(() => ({
|
||||
left: `${saturation.value}%`,
|
||||
top: `${100 - value.value}%`
|
||||
}))
|
||||
|
||||
function clamp(v: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, v))
|
||||
}
|
||||
|
||||
function updateFromPointer(e: PointerEvent) {
|
||||
const el = containerRef.value
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height))
|
||||
saturation.value = Math.round(x * 100)
|
||||
value.value = Math.round((1 - y) * 100)
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!(e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) return
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const step = e.shiftKey ? 10 : 1
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
saturation.value = clamp(saturation.value - step, 0, 100)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
saturation.value = clamp(saturation.value + step, 0, 100)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
value.value = clamp(value.value + step, 0, 100)
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
value.value = clamp(value.value - step, 0, 100)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
:aria-label="t('colorPicker.saturationValue')"
|
||||
:aria-valuetext="`${t('colorPicker.saturation')}: ${saturation}%, ${t('colorPicker.brightness')}: ${value}%`"
|
||||
class="relative aspect-square w-full cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-highlight"
|
||||
:style="{ backgroundColor: hueBackground, touchAction: 'none' }"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 rounded-sm bg-linear-to-r from-white to-transparent"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 rounded-sm bg-linear-to-b from-transparent to-black"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute size-3.5 -translate-1/2 rounded-full border-2 border-white shadow-[0_0_2px_rgba(0,0,0,0.6)]"
|
||||
:style="handleStyle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
115
src/components/ui/color-picker/ColorPickerSlider.vue
Normal file
115
src/components/ui/color-picker/ColorPickerSlider.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { hsbToRgb, rgbToHex } from '@/utils/colorUtil'
|
||||
|
||||
const {
|
||||
type,
|
||||
hue = 0,
|
||||
saturation = 100,
|
||||
brightness = 100
|
||||
} = defineProps<{
|
||||
type: 'hue' | 'alpha'
|
||||
hue?: number
|
||||
saturation?: number
|
||||
brightness?: number
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<number>({ required: true })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const max = computed(() => (type === 'hue' ? 360 : 100))
|
||||
|
||||
const fraction = computed(() => modelValue.value / max.value)
|
||||
|
||||
const trackBackground = computed(() => {
|
||||
if (type === 'hue') {
|
||||
return 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)'
|
||||
}
|
||||
const rgb = hsbToRgb({ h: hue, s: saturation, b: brightness })
|
||||
const hex = rgbToHex(rgb)
|
||||
return `linear-gradient(to right, transparent, ${hex})`
|
||||
})
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (type === 'alpha') {
|
||||
return {
|
||||
backgroundImage:
|
||||
'repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%)',
|
||||
backgroundSize: '8px 8px',
|
||||
touchAction: 'none'
|
||||
}
|
||||
}
|
||||
return {
|
||||
background: trackBackground.value,
|
||||
touchAction: 'none'
|
||||
}
|
||||
})
|
||||
|
||||
const ariaLabel = computed(() =>
|
||||
type === 'hue' ? t('colorPicker.hue') : t('colorPicker.alpha')
|
||||
)
|
||||
|
||||
function clamp(v: number, min: number, maxVal: number) {
|
||||
return Math.max(min, Math.min(maxVal, v))
|
||||
}
|
||||
|
||||
function updateFromPointer(e: PointerEvent) {
|
||||
const el = e.currentTarget as HTMLElement
|
||||
const rect = el.getBoundingClientRect()
|
||||
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||
modelValue.value = Math.round(x * max.value)
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!(e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) return
|
||||
updateFromPointer(e)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
const step = e.shiftKey ? 10 : 1
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
modelValue.value = clamp(modelValue.value - step, 0, max.value)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
modelValue.value = clamp(modelValue.value + step, 0, max.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
:aria-label="ariaLabel"
|
||||
:aria-valuenow="modelValue"
|
||||
:aria-valuemin="0"
|
||||
:aria-valuemax="max"
|
||||
class="relative flex h-4 cursor-pointer items-center rounded-full p-px outline-none focus-visible:ring-2 focus-visible:ring-highlight"
|
||||
:style="containerStyle"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div
|
||||
v-if="type === 'alpha'"
|
||||
class="absolute inset-0 rounded-full"
|
||||
:style="{ background: trackBackground }"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute aspect-square h-full -translate-x-1/2 rounded-full border-2 border-white shadow-[0_0_2px_rgba(0,0,0,0.6)]"
|
||||
:style="{ left: `${fraction * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -397,8 +397,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
if (app.canvas.empty) {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.emptyCanvas'),
|
||||
life: 3000
|
||||
summary: t('toastMessages.emptyCanvas')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -557,8 +556,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToQueue'),
|
||||
detail: t('toastMessages.pleaseSelectOutputNodes'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.pleaseSelectOutputNodes')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -571,8 +569,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.failedToQueue'),
|
||||
detail: t('toastMessages.failedExecutionPathResolution'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedExecutionPathResolution')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -602,8 +599,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToGroup'),
|
||||
detail: t('toastMessages.pleaseSelectNodesToGroup'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.pleaseSelectNodesToGroup')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -962,8 +958,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.notAvailable'),
|
||||
life: 3000
|
||||
detail: t('manager.notAvailable')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1048,8 +1043,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.cannotCreateSubgraph'),
|
||||
detail: t('toastMessages.failedToConvertToSubgraph'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedToConvertToSubgraph')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1258,8 +1252,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModels'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1278,8 +1271,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
summary: t('g.error'),
|
||||
detail: t('g.commandProhibited', {
|
||||
command: 'Comfy.Memory.UnloadModelsAndExecutionCache'
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -81,8 +81,7 @@ function getParentNodes(): SubgraphNode[] {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('subgraphStore.promoteOutsideSubgraph'),
|
||||
life: 2000
|
||||
detail: t('subgraphStore.promoteOutsideSubgraph')
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
161
src/core/graph/subgraph/resolveSubgraphInputTarget.test.ts
Normal file
161
src/core/graph/subgraph/resolveSubgraphInputTarget.test.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({})
|
||||
}))
|
||||
vi.mock('@/stores/domWidgetStore', () => ({
|
||||
useDomWidgetStore: () => ({ widgetStates: new Map() })
|
||||
}))
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: () => ({ updatePreviews: () => ({}) })
|
||||
}))
|
||||
|
||||
function createOuterSubgraphSetup(inputNames: string[]): {
|
||||
outerSubgraph: Subgraph
|
||||
outerSubgraphNode: SubgraphNode
|
||||
} {
|
||||
const outerSubgraph = createTestSubgraph({
|
||||
inputs: inputNames.map((name) => ({ name, type: '*' }))
|
||||
})
|
||||
const outerSubgraphNode = createTestSubgraphNode(outerSubgraph, { id: 1 })
|
||||
return { outerSubgraph, outerSubgraphNode }
|
||||
}
|
||||
|
||||
function addLinkedNestedSubgraphNode(
|
||||
outerSubgraph: Subgraph,
|
||||
inputName: string,
|
||||
linkedInputName: string,
|
||||
options: { widget?: string } = {}
|
||||
): { innerSubgraphNode: SubgraphNode } {
|
||||
const innerSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: linkedInputName, type: '*' }]
|
||||
})
|
||||
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, { id: 819 })
|
||||
outerSubgraph.add(innerSubgraphNode)
|
||||
|
||||
const inputSlot = outerSubgraph.inputNode.slots.find(
|
||||
(slot) => slot.name === inputName
|
||||
)
|
||||
if (!inputSlot) throw new Error(`Missing subgraph input slot: ${inputName}`)
|
||||
|
||||
const input = innerSubgraphNode.addInput(linkedInputName, '*')
|
||||
if (options.widget) {
|
||||
innerSubgraphNode.addWidget('number', options.widget, 0, () => undefined)
|
||||
input.widget = { name: options.widget }
|
||||
}
|
||||
inputSlot.connect(input, innerSubgraphNode)
|
||||
|
||||
if (input.link == null) {
|
||||
throw new Error(`Expected link to be created for input ${linkedInputName}`)
|
||||
}
|
||||
|
||||
return { innerSubgraphNode }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('resolveSubgraphInputTarget', () => {
|
||||
test('returns target for widget-backed input on nested SubgraphNode', () => {
|
||||
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
|
||||
'width'
|
||||
])
|
||||
addLinkedNestedSubgraphNode(outerSubgraph, 'width', 'width', {
|
||||
widget: 'width'
|
||||
})
|
||||
|
||||
const result = resolveSubgraphInputTarget(outerSubgraphNode, 'width')
|
||||
|
||||
expect(result).toMatchObject({
|
||||
nodeId: '819',
|
||||
widgetName: 'width'
|
||||
})
|
||||
})
|
||||
|
||||
test('returns undefined for non-widget input on nested SubgraphNode', () => {
|
||||
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
|
||||
'audio'
|
||||
])
|
||||
addLinkedNestedSubgraphNode(outerSubgraph, 'audio', 'audio')
|
||||
|
||||
const result = resolveSubgraphInputTarget(outerSubgraphNode, 'audio')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
test('resolves widget inputs but not non-widget inputs on the same nested SubgraphNode', () => {
|
||||
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
|
||||
'width',
|
||||
'audio'
|
||||
])
|
||||
addLinkedNestedSubgraphNode(outerSubgraph, 'width', 'width', {
|
||||
widget: 'width'
|
||||
})
|
||||
addLinkedNestedSubgraphNode(outerSubgraph, 'audio', 'audio')
|
||||
|
||||
expect(
|
||||
resolveSubgraphInputTarget(outerSubgraphNode, 'width')
|
||||
).toMatchObject({
|
||||
nodeId: '819',
|
||||
widgetName: 'width'
|
||||
})
|
||||
expect(
|
||||
resolveSubgraphInputTarget(outerSubgraphNode, 'audio')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
test('returns target for widget-backed input on plain interior node', () => {
|
||||
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
|
||||
'seed'
|
||||
])
|
||||
|
||||
const inputSlot = outerSubgraph.inputNode.slots.find(
|
||||
(slot) => slot.name === 'seed'
|
||||
)!
|
||||
const node = new LGraphNode('Interior-seed')
|
||||
node.id = 42
|
||||
const input = node.addInput('seed_input', '*')
|
||||
node.addWidget('number', 'seed', 0, () => undefined)
|
||||
input.widget = { name: 'seed' }
|
||||
outerSubgraph.add(node)
|
||||
inputSlot.connect(input, node)
|
||||
|
||||
const result = resolveSubgraphInputTarget(outerSubgraphNode, 'seed')
|
||||
|
||||
expect(result).toMatchObject({
|
||||
nodeId: '42',
|
||||
widgetName: 'seed'
|
||||
})
|
||||
})
|
||||
|
||||
test('returns undefined for non-widget input on plain interior node', () => {
|
||||
const { outerSubgraph, outerSubgraphNode } = createOuterSubgraphSetup([
|
||||
'image'
|
||||
])
|
||||
|
||||
const inputSlot = outerSubgraph.inputNode.slots.find(
|
||||
(slot) => slot.name === 'image'
|
||||
)!
|
||||
const node = new LGraphNode('Interior-image')
|
||||
const input = node.addInput('image_input', '*')
|
||||
outerSubgraph.add(node)
|
||||
inputSlot.connect(input, node)
|
||||
|
||||
const result = resolveSubgraphInputTarget(outerSubgraphNode, 'image')
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,9 @@ export function resolveSubgraphInputTarget(
|
||||
inputName,
|
||||
({ inputNode, targetInput, getTargetWidget }) => {
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
const targetWidget = getTargetWidget()
|
||||
if (!targetWidget) return undefined
|
||||
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
widgetName: targetInput.name
|
||||
|
||||
@@ -204,8 +204,7 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('desktopUpdate.errorInstallingUpdate'),
|
||||
life: 10_000
|
||||
detail: t('desktopUpdate.errorInstallingUpdate')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -214,8 +213,7 @@ import { electronAPI as getElectronAPI } from '@/utils/envUtil'
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('desktopUpdate.errorCheckingUpdate'),
|
||||
life: 10_000
|
||||
detail: t('desktopUpdate.errorCheckingUpdate')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "طي الكل",
|
||||
"color": "اللون",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "شعار ComfyOrg",
|
||||
"comingSoon": "قريباً",
|
||||
"command": "أمر",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "عنصر الواجهة غير مرئي"
|
||||
},
|
||||
"cancelThisRun": "إلغاء هذا التشغيل",
|
||||
"deleteAllAssets": "حذف جميع الأصول من هذه الجلسة",
|
||||
"downloadAll": "تنزيل الكل",
|
||||
"dragAndDropImage": "اسحب وأسقط صورة",
|
||||
"emptyWorkflowExplanation": "سير العمل الخاص بك فارغ. تحتاج إلى بعض العقد أولاً لبدء بناء التطبيق.",
|
||||
"enterNodeGraph": "دخول مخطط العقد",
|
||||
"giveFeedback": "إعطاء ملاحظات",
|
||||
"graphMode": "وضع الرسم البياني",
|
||||
"hasCreditCost": "يتطلب أرصدة إضافية",
|
||||
"linearMode": "وضع التطبيق",
|
||||
"loadTemplate": "تحميل قالب",
|
||||
"mobileControls": "تعديل وتشغيل",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "تمت إضافتك إلى {workspaceName}",
|
||||
"inviteAccepted": "تم قبول الدعوة",
|
||||
"inviteFailed": "فشل في قبول الدعوة",
|
||||
"unsavedChanges": {
|
||||
"message": "لديك تغييرات غير محفوظة. هل تريد تجاهلها والانتقال إلى مساحة عمل أخرى؟",
|
||||
"title": "تغييرات غير محفوظة"
|
||||
},
|
||||
"switchFailed": "فشل في تبديل مساحة العمل. يرجى المحاولة مرة أخرى.",
|
||||
"viewWorkspace": "عرض مساحة العمل"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "تخطيط شريط التبويبات",
|
||||
"options": {
|
||||
"Default": "افتراضي",
|
||||
"Integrated": "مُدمج"
|
||||
"Legacy": "تقليدي"
|
||||
},
|
||||
"tooltip": "يتحكم في تخطيط شريط التبويبات. \"مُدمج\" ينقل عناصر المساعدة والتحكمات الخاصة بالمستخدم إلى منطقة شريط التبويبات."
|
||||
},
|
||||
|
||||
@@ -483,6 +483,13 @@
|
||||
"black": "Black",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"colorPicker": {
|
||||
"saturationValue": "Saturation and brightness",
|
||||
"saturation": "Saturation",
|
||||
"brightness": "Brightness",
|
||||
"hue": "Hue",
|
||||
"alpha": "Alpha"
|
||||
},
|
||||
"contextMenu": {
|
||||
"Inputs": "Inputs",
|
||||
"Outputs": "Outputs",
|
||||
@@ -3179,6 +3186,7 @@
|
||||
"loadTemplate": "Load a template",
|
||||
"cancelThisRun": "Cancel this run",
|
||||
"deleteAllAssets": "Delete all assets from this run",
|
||||
"hasCreditCost": "Requires additional credits",
|
||||
"welcome": {
|
||||
"title": "App Mode",
|
||||
"message": "A simplified view that hides the node graph so you can focus on creating.",
|
||||
@@ -3405,14 +3413,11 @@
|
||||
"retryDownload": "Retry download"
|
||||
},
|
||||
"workspace": {
|
||||
"unsavedChanges": {
|
||||
"title": "Unsaved Changes",
|
||||
"message": "You have unsaved changes. Do you want to discard them and switch workspaces?"
|
||||
},
|
||||
"inviteAccepted": "Invite Accepted",
|
||||
"addedToWorkspace": "You have been added to:",
|
||||
"inviteFailed": "Failed to Accept Invite",
|
||||
"viewWorkspace": "View workspace"
|
||||
"viewWorkspace": "View workspace",
|
||||
"switchFailed": "Failed to switch workspace. Please try again."
|
||||
},
|
||||
"workspaceAuth": {
|
||||
"errors": {
|
||||
|
||||
@@ -398,10 +398,10 @@
|
||||
},
|
||||
"Comfy_UI_TabBarLayout": {
|
||||
"name": "Tab Bar Layout",
|
||||
"tooltip": "Controls the layout of the tab bar. \"Integrated\" moves Help and User controls into the tab bar area.",
|
||||
"tooltip": "Controls the elements contained in the integrated tab bar.",
|
||||
"options": {
|
||||
"Default": "Default",
|
||||
"Integrated": "Integrated"
|
||||
"Legacy": "Legacy"
|
||||
}
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Colapsar todo",
|
||||
"color": "Color",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Logo de ComfyOrg",
|
||||
"comingSoon": "Próximamente",
|
||||
"command": "Comando",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "Widget no visible"
|
||||
},
|
||||
"cancelThisRun": "Cancelar esta ejecución",
|
||||
"deleteAllAssets": "Eliminar todos los recursos de esta ejecución",
|
||||
"downloadAll": "Descargar todo",
|
||||
"dragAndDropImage": "Arrastra y suelta una imagen",
|
||||
"emptyWorkflowExplanation": "Tu flujo de trabajo está vacío. Necesitas algunos nodos primero para empezar a construir una aplicación.",
|
||||
"enterNodeGraph": "Entrar al grafo de nodos",
|
||||
"giveFeedback": "Enviar comentarios",
|
||||
"graphMode": "Modo gráfico",
|
||||
"hasCreditCost": "Requiere créditos adicionales",
|
||||
"linearMode": "Modo App",
|
||||
"loadTemplate": "Cargar una plantilla",
|
||||
"mobileControls": "Editar y ejecutar",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "Has sido añadido a {workspaceName}",
|
||||
"inviteAccepted": "Invitación aceptada",
|
||||
"inviteFailed": "No se pudo aceptar la invitación",
|
||||
"unsavedChanges": {
|
||||
"message": "Tienes cambios no guardados. ¿Quieres descartarlos y cambiar de espacio de trabajo?",
|
||||
"title": "Cambios no guardados"
|
||||
},
|
||||
"switchFailed": "No se pudo cambiar de espacio de trabajo. Por favor, inténtalo de nuevo.",
|
||||
"viewWorkspace": "Ver espacio de trabajo"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Diseño de barra de pestañas",
|
||||
"options": {
|
||||
"Default": "Predeterminado",
|
||||
"Integrated": "Integrado"
|
||||
"Legacy": "Clásico"
|
||||
},
|
||||
"tooltip": "Controla el diseño de la barra de pestañas. \"Integrado\" mueve los controles de Ayuda y Usuario al área de la barra de pestañas."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "بستن همه",
|
||||
"color": "رنگ",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "لوگوی ComfyOrg",
|
||||
"comingSoon": "بهزودی",
|
||||
"command": "دستور",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "ویجت قابل مشاهده نیست"
|
||||
},
|
||||
"cancelThisRun": "لغو این اجرا",
|
||||
"deleteAllAssets": "حذف همه داراییها از این اجرا",
|
||||
"downloadAll": "دانلود همه",
|
||||
"dragAndDropImage": "تصویر را بکشید و رها کنید",
|
||||
"emptyWorkflowExplanation": "جریان کاری شما خالی است. ابتدا باید چند node اضافه کنید تا بتوانید یک برنامه بسازید.",
|
||||
"enterNodeGraph": "ورود به گراف node",
|
||||
"giveFeedback": "ارسال بازخورد",
|
||||
"graphMode": "حالت گراف",
|
||||
"hasCreditCost": "نیازمند اعتبار اضافی",
|
||||
"linearMode": "حالت برنامه",
|
||||
"loadTemplate": "بارگذاری قالب",
|
||||
"mobileControls": "ویرایش و اجرا",
|
||||
@@ -3385,10 +3388,7 @@
|
||||
"addedToWorkspace": "شما به {workspaceName} اضافه شدید",
|
||||
"inviteAccepted": "دعوت پذیرفته شد",
|
||||
"inviteFailed": "پذیرش دعوت ناموفق بود",
|
||||
"unsavedChanges": {
|
||||
"message": "شما تغییرات ذخیرهنشده دارید. آیا میخواهید آنها را رها کرده و فضای کاری را تغییر دهید؟",
|
||||
"title": "تغییرات ذخیرهنشده"
|
||||
},
|
||||
"switchFailed": "تغییر ورکاسپیس ناموفق بود. لطفاً دوباره تلاش کنید.",
|
||||
"viewWorkspace": "مشاهده workspace"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "چیدمان نوار تب",
|
||||
"options": {
|
||||
"Default": "پیشفرض",
|
||||
"Integrated": "یکپارچه"
|
||||
"Legacy": "قدیمی"
|
||||
},
|
||||
"tooltip": "چیدمان نوار تب را کنترل میکند. «یکپارچه» کنترلهای راهنما و کاربر را به ناحیه نوار تب منتقل میکند."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Tout réduire",
|
||||
"color": "Couleur",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Logo ComfyOrg",
|
||||
"comingSoon": "Bientôt disponible",
|
||||
"command": "Commande",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "Widget non visible"
|
||||
},
|
||||
"cancelThisRun": "Annuler cette exécution",
|
||||
"deleteAllAssets": "Supprimer toutes les ressources de cette exécution",
|
||||
"downloadAll": "Tout télécharger",
|
||||
"dragAndDropImage": "Glissez-déposez une image",
|
||||
"emptyWorkflowExplanation": "Votre workflow est vide. Vous devez d'abord ajouter des nodes pour commencer à créer une application.",
|
||||
"enterNodeGraph": "Entrer dans le graphique de nœuds",
|
||||
"giveFeedback": "Donner un avis",
|
||||
"graphMode": "Mode graphique",
|
||||
"hasCreditCost": "Nécessite des crédits supplémentaires",
|
||||
"linearMode": "Mode App",
|
||||
"loadTemplate": "Charger un modèle",
|
||||
"mobileControls": "Éditer & Exécuter",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "Vous avez été ajouté à {workspaceName}",
|
||||
"inviteAccepted": "Invitation acceptée",
|
||||
"inviteFailed": "Échec de l'acceptation de l'invitation",
|
||||
"unsavedChanges": {
|
||||
"message": "Vous avez des modifications non enregistrées. Voulez-vous les abandonner et changer d’espace de travail ?",
|
||||
"title": "Modifications non enregistrées"
|
||||
},
|
||||
"switchFailed": "Échec du changement d’espace de travail. Veuillez réessayer.",
|
||||
"viewWorkspace": "Voir l’espace de travail"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Disposition de la barre d’onglets",
|
||||
"options": {
|
||||
"Default": "Par défaut",
|
||||
"Integrated": "Intégrée"
|
||||
"Legacy": "Héritage"
|
||||
},
|
||||
"tooltip": "Contrôle la disposition de la barre d’onglets. « Intégrée » déplace les contrôles Aide et Utilisateur dans la zone de la barre d’onglets."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "すべて折りたたむ",
|
||||
"color": "色",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrgロゴ",
|
||||
"comingSoon": "近日公開",
|
||||
"command": "コマンド",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "ウィジェットが表示されていません"
|
||||
},
|
||||
"cancelThisRun": "この実行をキャンセル",
|
||||
"deleteAllAssets": "この実行からすべてのアセットを削除",
|
||||
"downloadAll": "すべてダウンロード",
|
||||
"dragAndDropImage": "画像をドラッグ&ドロップ",
|
||||
"emptyWorkflowExplanation": "ワークフローが空です。アプリを作成するには、まずノードを追加してください。",
|
||||
"enterNodeGraph": "ノードグラフに入る",
|
||||
"giveFeedback": "フィードバックを送る",
|
||||
"graphMode": "グラフモード",
|
||||
"hasCreditCost": "追加クレジットが必要です",
|
||||
"linearMode": "アプリモード",
|
||||
"loadTemplate": "テンプレートを読み込む",
|
||||
"mobileControls": "編集と実行",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "{workspaceName}に追加されました",
|
||||
"inviteAccepted": "招待を承諾しました",
|
||||
"inviteFailed": "招待の承諾に失敗しました",
|
||||
"unsavedChanges": {
|
||||
"message": "未保存の変更があります。破棄してワークスペースを切り替えますか?",
|
||||
"title": "未保存の変更"
|
||||
},
|
||||
"switchFailed": "ワークスペースの切り替えに失敗しました。もう一度お試しください。",
|
||||
"viewWorkspace": "ワークスペースを見る"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "タブバーのレイアウト",
|
||||
"options": {
|
||||
"Default": "デフォルト",
|
||||
"Integrated": "統合"
|
||||
"Legacy": "レガシー"
|
||||
},
|
||||
"tooltip": "タブバーのレイアウトを制御します。「統合」を選択すると、ヘルプとユーザーコントロールがタブバーエリアに移動します。"
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "모두 접기",
|
||||
"color": "색상",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg 로고",
|
||||
"comingSoon": "곧 출시 예정",
|
||||
"command": "명령",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "위젯이 표시되지 않습니다"
|
||||
},
|
||||
"cancelThisRun": "이 실행 취소",
|
||||
"deleteAllAssets": "이 실행에서 모든 에셋 삭제",
|
||||
"downloadAll": "모두 다운로드",
|
||||
"dragAndDropImage": "이미지를 드래그 앤 드롭하세요",
|
||||
"emptyWorkflowExplanation": "워크플로우가 비어 있습니다. 앱을 만들려면 먼저 노드를 추가해야 합니다.",
|
||||
"enterNodeGraph": "노드 그래프로 진입",
|
||||
"giveFeedback": "피드백 보내기",
|
||||
"graphMode": "그래프 모드",
|
||||
"hasCreditCost": "추가 크레딧 필요",
|
||||
"linearMode": "앱 모드",
|
||||
"loadTemplate": "템플릿 불러오기",
|
||||
"mobileControls": "편집 및 실행",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "{workspaceName} 워크스페이스에 추가되었습니다",
|
||||
"inviteAccepted": "초대 수락됨",
|
||||
"inviteFailed": "초대 수락에 실패했습니다",
|
||||
"unsavedChanges": {
|
||||
"message": "저장되지 않은 변경 사항이 있습니다. 변경 사항을 취소하고 워크스페이스를 전환하시겠습니까?",
|
||||
"title": "저장되지 않은 변경 사항"
|
||||
},
|
||||
"switchFailed": "워크스페이스 전환에 실패했습니다. 다시 시도해 주세요.",
|
||||
"viewWorkspace": "워크스페이스 보기"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "탭 바 레이아웃",
|
||||
"options": {
|
||||
"Default": "기본값",
|
||||
"Integrated": "통합"
|
||||
"Legacy": "레거시"
|
||||
},
|
||||
"tooltip": "탭 바의 레이아웃을 제어합니다. \"통합\"을 선택하면 도움말과 사용자 컨트롤이 탭 바 영역으로 이동합니다."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Recolher tudo",
|
||||
"color": "Cor",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Logo do ComfyOrg",
|
||||
"comingSoon": "Em breve",
|
||||
"command": "Comando",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "Widget não visível"
|
||||
},
|
||||
"cancelThisRun": "Cancelar esta execução",
|
||||
"deleteAllAssets": "Excluir todos os ativos desta execução",
|
||||
"downloadAll": "Baixar tudo",
|
||||
"dragAndDropImage": "Arraste e solte uma imagem",
|
||||
"emptyWorkflowExplanation": "Seu fluxo de trabalho está vazio. Você precisa adicionar alguns nós primeiro para começar a construir um app.",
|
||||
"enterNodeGraph": "Entrar no grafo de nós",
|
||||
"giveFeedback": "Enviar feedback",
|
||||
"graphMode": "Modo Gráfico",
|
||||
"hasCreditCost": "Requer créditos adicionais",
|
||||
"linearMode": "Modo App",
|
||||
"loadTemplate": "Carregar um modelo",
|
||||
"mobileControls": "Editar e Executar",
|
||||
@@ -3385,10 +3388,7 @@
|
||||
"addedToWorkspace": "Você foi adicionado ao {workspaceName}",
|
||||
"inviteAccepted": "Convite aceito",
|
||||
"inviteFailed": "Falha ao aceitar convite",
|
||||
"unsavedChanges": {
|
||||
"message": "Você tem alterações não salvas. Deseja descartá-las e trocar de espaço de trabalho?",
|
||||
"title": "Alterações não salvas"
|
||||
},
|
||||
"switchFailed": "Falha ao alternar o workspace. Por favor, tente novamente.",
|
||||
"viewWorkspace": "Ver workspace"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Layout da Barra de Abas",
|
||||
"options": {
|
||||
"Default": "Padrão",
|
||||
"Integrated": "Integrado"
|
||||
"Legacy": "Legado"
|
||||
},
|
||||
"tooltip": "Controla o layout da barra de abas. \"Integrado\" move os controles de Ajuda e Usuário para a área da barra de abas."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Свернуть все",
|
||||
"color": "Цвет",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "Логотип ComfyOrg",
|
||||
"comingSoon": "Скоро будет",
|
||||
"command": "Команда",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "Виджет не отображается"
|
||||
},
|
||||
"cancelThisRun": "Отменить этот запуск",
|
||||
"deleteAllAssets": "Удалить все ресурсы из этого запуска",
|
||||
"downloadAll": "Скачать всё",
|
||||
"dragAndDropImage": "Перетащите изображение",
|
||||
"emptyWorkflowExplanation": "Ваш рабочий процесс пуст. Сначала добавьте несколько узлов, чтобы начать создавать приложение.",
|
||||
"enterNodeGraph": "Войти в граф узлов",
|
||||
"giveFeedback": "Оставить отзыв",
|
||||
"graphMode": "Графовый режим",
|
||||
"hasCreditCost": "Требуются дополнительные кредиты",
|
||||
"linearMode": "Режим приложения",
|
||||
"loadTemplate": "Загрузить шаблон",
|
||||
"mobileControls": "Редактировать и запустить",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "Вы были добавлены в {workspaceName}",
|
||||
"inviteAccepted": "Приглашение принято",
|
||||
"inviteFailed": "Не удалось принять приглашение",
|
||||
"unsavedChanges": {
|
||||
"message": "У вас есть несохранённые изменения. Хотите их отменить и переключиться на другое рабочее пространство?",
|
||||
"title": "Несохранённые изменения"
|
||||
},
|
||||
"switchFailed": "Не удалось переключить рабочее пространство. Пожалуйста, попробуйте еще раз.",
|
||||
"viewWorkspace": "Просмотреть рабочее пространство"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Макет панели вкладок",
|
||||
"options": {
|
||||
"Default": "По умолчанию",
|
||||
"Integrated": "Интегрированный"
|
||||
"Legacy": "Классический"
|
||||
},
|
||||
"tooltip": "Управляет расположением панели вкладок. «Интегрированный» перемещает элементы управления Справкой и Пользователем в область панели вкладок."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "Hepsini daralt",
|
||||
"color": "Renk",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg Logosu",
|
||||
"comingSoon": "Çok Yakında",
|
||||
"command": "Komut",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "Widget görünür değil"
|
||||
},
|
||||
"cancelThisRun": "Bu çalıştırmayı iptal et",
|
||||
"deleteAllAssets": "Bu çalışmadaki tüm varlıkları sil",
|
||||
"downloadAll": "Tümünü İndir",
|
||||
"dragAndDropImage": "Bir görseli sürükleyip bırakın",
|
||||
"emptyWorkflowExplanation": "Çalışma akışınız boş. Bir uygulama oluşturmaya başlamak için önce bazı düğümler eklemelisiniz.",
|
||||
"enterNodeGraph": "Düğüm grafiğine gir",
|
||||
"giveFeedback": "Geri bildirim ver",
|
||||
"graphMode": "Grafik Modu",
|
||||
"hasCreditCost": "Ekstra kredi gerektirir",
|
||||
"linearMode": "Uygulama Modu",
|
||||
"loadTemplate": "Şablon yükle",
|
||||
"mobileControls": "Düzenle ve Çalıştır",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "{workspaceName} çalışma alanına eklendiniz",
|
||||
"inviteAccepted": "Davet kabul edildi",
|
||||
"inviteFailed": "Davet kabul edilemedi",
|
||||
"unsavedChanges": {
|
||||
"message": "Kaydedilmemiş değişiklikleriniz var. Bunları iptal edip çalışma alanlarını değiştirmek istiyor musunuz?",
|
||||
"title": "Kaydedilmemiş Değişiklikler"
|
||||
},
|
||||
"switchFailed": "Çalışma alanı değiştirilemedi. Lütfen tekrar deneyin.",
|
||||
"viewWorkspace": "Çalışma alanını görüntüle"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "Sekme Çubuğu Düzeni",
|
||||
"options": {
|
||||
"Default": "Varsayılan",
|
||||
"Integrated": "Entegre"
|
||||
"Legacy": "Klasik"
|
||||
},
|
||||
"tooltip": "Sekme çubuğu düzenini kontrol eder. \"Entegre\" seçeneği, Yardım ve Kullanıcı kontrollerini sekme çubuğu alanına taşır."
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "全部摺疊",
|
||||
"color": "顏色",
|
||||
"comfy": "Comfy",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg 標誌",
|
||||
"comingSoon": "即將推出",
|
||||
"command": "指令",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "元件不可見"
|
||||
},
|
||||
"cancelThisRun": "取消本次執行",
|
||||
"deleteAllAssets": "刪除本次運行的所有資產",
|
||||
"downloadAll": "全部下載",
|
||||
"dragAndDropImage": "拖曳圖片到此",
|
||||
"emptyWorkflowExplanation": "您的工作流程目前是空的。您需要先新增一些節點,才能開始建立應用程式。",
|
||||
"enterNodeGraph": "進入節點圖",
|
||||
"giveFeedback": "提供回饋",
|
||||
"graphMode": "圖形模式",
|
||||
"hasCreditCost": "需要額外點數",
|
||||
"linearMode": "App 模式",
|
||||
"loadTemplate": "載入範本",
|
||||
"mobileControls": "編輯與執行",
|
||||
@@ -3373,10 +3376,7 @@
|
||||
"addedToWorkspace": "你已被加入 {workspaceName}",
|
||||
"inviteAccepted": "已接受邀請",
|
||||
"inviteFailed": "接受邀請失敗",
|
||||
"unsavedChanges": {
|
||||
"message": "您有未儲存的變更。是否要捨棄這些變更並切換工作區?",
|
||||
"title": "未儲存的變更"
|
||||
},
|
||||
"switchFailed": "切換工作區失敗。請再試一次。",
|
||||
"viewWorkspace": "檢視工作區"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "分頁列佈局",
|
||||
"options": {
|
||||
"Default": "預設",
|
||||
"Integrated": "整合"
|
||||
"Legacy": "傳統"
|
||||
},
|
||||
"tooltip": "控制分頁列的佈局。「整合」會將說明和使用者控制項移至分頁列區域。"
|
||||
},
|
||||
|
||||
@@ -952,6 +952,7 @@
|
||||
"collapseAll": "全部折叠",
|
||||
"color": "颜色",
|
||||
"comfy": "舒适",
|
||||
"comfyCloud": "Comfy Cloud",
|
||||
"comfyOrgLogoAlt": "ComfyOrg 徽标",
|
||||
"comingSoon": "即将推出",
|
||||
"command": "指令",
|
||||
@@ -1459,12 +1460,14 @@
|
||||
"unknownWidget": "组件不可见"
|
||||
},
|
||||
"cancelThisRun": "取消本次运行",
|
||||
"deleteAllAssets": "删除本次运行的所有资源",
|
||||
"downloadAll": "全部下载",
|
||||
"dragAndDropImage": "拖拽图片到此处",
|
||||
"emptyWorkflowExplanation": "你的工作流为空。你需要先添加一些节点,才能开始构建应用。",
|
||||
"enterNodeGraph": "进入节点图",
|
||||
"giveFeedback": "提供反馈",
|
||||
"graphMode": "图形模式",
|
||||
"hasCreditCost": "需要额外积分",
|
||||
"linearMode": "App 模式",
|
||||
"loadTemplate": "加载模板",
|
||||
"mobileControls": "编辑与运行",
|
||||
@@ -3385,10 +3388,7 @@
|
||||
"addedToWorkspace": "您已被加入 {workspaceName}",
|
||||
"inviteAccepted": "邀请已接受",
|
||||
"inviteFailed": "接受邀请失败",
|
||||
"unsavedChanges": {
|
||||
"message": "您有未保存的更改。是否要放弃这些更改并切换工作区?",
|
||||
"title": "未保存的更改"
|
||||
},
|
||||
"switchFailed": "切换工作区失败。请重试。",
|
||||
"viewWorkspace": "查看工作区"
|
||||
},
|
||||
"workspaceAuth": {
|
||||
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "标签栏布局",
|
||||
"options": {
|
||||
"Default": "默认",
|
||||
"Integrated": "集成"
|
||||
"Legacy": "传统"
|
||||
},
|
||||
"tooltip": "控制标签栏的布局。“集成”会将帮助和用户控件移动到标签栏区域。"
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@ const tooltipDelay = computed<number>(() =>
|
||||
|
||||
const { isLoading, error } = useImage({
|
||||
src: asset.preview_url ?? '',
|
||||
alt: asset.name
|
||||
alt: asset.display_name || asset.name
|
||||
})
|
||||
|
||||
function handleSelect() {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:aria-label="
|
||||
asset
|
||||
? $t('assetBrowser.ariaLabel.assetCard', {
|
||||
name: asset.name,
|
||||
name: asset.display_name || asset.name,
|
||||
type: fileKind
|
||||
})
|
||||
: $t('assetBrowser.ariaLabel.loadingAsset')
|
||||
@@ -225,7 +225,7 @@ const canInspect = computed(() => isPreviewableMediaType(fileKind.value))
|
||||
|
||||
// Get filename without extension
|
||||
const fileName = computed(() => {
|
||||
return getFilenameDetails(asset?.name || '').filename
|
||||
return getFilenameDetails(asset?.display_name || asset?.name || '').filename
|
||||
})
|
||||
|
||||
// Adapt AssetItem to legacy AssetMeta format for existing components
|
||||
@@ -234,6 +234,7 @@ const adaptedAsset = computed(() => {
|
||||
return {
|
||||
id: asset.id,
|
||||
name: asset.name,
|
||||
display_name: asset.display_name,
|
||||
kind: fileKind.value,
|
||||
src: asset.preview_url || '',
|
||||
size: asset.size,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<img
|
||||
v-if="!error"
|
||||
:src="asset.src"
|
||||
:alt="asset.name"
|
||||
:alt="asset.display_name || asset.name"
|
||||
class="size-full object-contain transition-transform duration-300 group-hover:scale-105 group-data-[selected=true]:scale-105"
|
||||
/>
|
||||
<div
|
||||
@@ -34,7 +34,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { state, error, isReady } = useImage({
|
||||
src: asset.src ?? '',
|
||||
alt: asset.name
|
||||
alt: asset.display_name || asset.name
|
||||
})
|
||||
|
||||
whenever(
|
||||
|
||||
@@ -39,6 +39,7 @@ export function mapTaskOutputToAssetItem(
|
||||
return {
|
||||
id: taskItem.jobId,
|
||||
name: output.filename,
|
||||
display_name: output.display_name,
|
||||
size: 0,
|
||||
created_at: taskItem.executionStartTimestamp
|
||||
? new Date(taskItem.executionStartTimestamp).toISOString()
|
||||
|
||||
@@ -68,7 +68,7 @@ export function useMediaAssetActions() {
|
||||
if (!targetAsset) return
|
||||
|
||||
try {
|
||||
const filename = targetAsset.name
|
||||
const filename = targetAsset.display_name || targetAsset.name
|
||||
// Prefer preview_url (already includes subfolder) with getAssetUrl as fallback
|
||||
const downloadUrl = targetAsset.preview_url || getAssetUrl(targetAsset)
|
||||
|
||||
@@ -84,8 +84,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000
|
||||
detail: t('g.failedToDownloadImage')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -110,7 +109,7 @@ export function useMediaAssetActions() {
|
||||
|
||||
try {
|
||||
assets.forEach((asset) => {
|
||||
const filename = asset.name
|
||||
const filename = asset.display_name || asset.name
|
||||
const downloadUrl = asset.preview_url || getAssetUrl(asset)
|
||||
downloadFile(downloadUrl, filename)
|
||||
})
|
||||
@@ -126,8 +125,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000
|
||||
detail: t('g.failedToDownloadImage')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -182,8 +180,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('exportToast.exportFailedSingle'),
|
||||
life: 3000
|
||||
detail: t('exportToast.exportFailedSingle')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -238,8 +235,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.nodeTypeNotFound', { nodeType }),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.nodeTypeNotFound', { nodeType })
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -252,8 +248,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.failedToCreateNode'),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.failedToCreateNode')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -443,8 +438,7 @@ export function useMediaAssetActions() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('mediaAsset.selection.failedToAddNodes'),
|
||||
life: 3000
|
||||
detail: t('mediaAsset.selection.failedToAddNodes')
|
||||
})
|
||||
} else {
|
||||
toast.add({
|
||||
@@ -676,8 +670,7 @@ export function useMediaAssetActions() {
|
||||
summary: t('g.error'),
|
||||
detail: isSingle
|
||||
? t('mediaAsset.failedToDeleteAsset')
|
||||
: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
life: 3000
|
||||
: t('mediaAsset.selection.failedToDeleteAssets')
|
||||
})
|
||||
} else {
|
||||
// Partial success (only possible with multiple assets)
|
||||
@@ -698,8 +691,7 @@ export function useMediaAssetActions() {
|
||||
summary: t('g.error'),
|
||||
detail: isSingle
|
||||
? t('mediaAsset.failedToDeleteAsset')
|
||||
: t('mediaAsset.selection.failedToDeleteAssets'),
|
||||
life: 3000
|
||||
: t('mediaAsset.selection.failedToDeleteAssets')
|
||||
})
|
||||
} finally {
|
||||
// Hide loading overlay for all assets
|
||||
|
||||
@@ -9,6 +9,7 @@ const zAsset = z.object({
|
||||
mime_type: z.string().nullish(),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
preview_id: z.string().nullable().optional(),
|
||||
display_name: z.string().optional(),
|
||||
preview_url: z.string().optional(),
|
||||
created_at: z.string().optional(),
|
||||
updated_at: z.string().optional(),
|
||||
|
||||
@@ -73,8 +73,7 @@ export function createAssetWidget(
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('assetBrowser.invalidAsset'),
|
||||
detail: t('assetBrowser.invalidAssetDetail'),
|
||||
life: 5000
|
||||
detail: t('assetBrowser.invalidAssetDetail')
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -92,8 +91,7 @@ export function createAssetWidget(
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('assetBrowser.invalidFilename'),
|
||||
detail: t('assetBrowser.invalidFilenameDetail'),
|
||||
life: 5000
|
||||
detail: t('assetBrowser.invalidFilenameDetail')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type OutputOverrides = Partial<{
|
||||
subfolder: string
|
||||
nodeId: string
|
||||
url: string
|
||||
display_name: string
|
||||
}>
|
||||
|
||||
function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
|
||||
@@ -32,7 +33,8 @@ function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
|
||||
}
|
||||
return {
|
||||
...merged,
|
||||
previewUrl: merged.url
|
||||
previewUrl: merged.url,
|
||||
display_name: merged.display_name
|
||||
} as ResultItemImpl
|
||||
}
|
||||
|
||||
@@ -125,6 +127,48 @@ describe('resolveOutputAssetItems', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('propagates display_name from output to asset item', async () => {
|
||||
const output = createOutput({
|
||||
filename: 'abc123hash.png',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/abc123hash.png',
|
||||
display_name: 'ComfyUI_00001_.png'
|
||||
})
|
||||
const metadata: OutputAssetMetadata = {
|
||||
jobId: 'job-dn',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
outputCount: 1,
|
||||
allOutputs: [output]
|
||||
}
|
||||
|
||||
const results = await resolveOutputAssetItems(metadata)
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].name).toBe('abc123hash.png')
|
||||
expect(results[0].display_name).toBe('ComfyUI_00001_.png')
|
||||
})
|
||||
|
||||
it('omits display_name when not present in output', async () => {
|
||||
const output = createOutput({
|
||||
filename: 'file.png',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/file.png'
|
||||
})
|
||||
const metadata: OutputAssetMetadata = {
|
||||
jobId: 'job-nodn',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
outputCount: 1,
|
||||
allOutputs: [output]
|
||||
}
|
||||
|
||||
const results = await resolveOutputAssetItems(metadata)
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].display_name).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps root outputs with empty subfolders', async () => {
|
||||
const output = createOutput({
|
||||
filename: 'root.png',
|
||||
|
||||
@@ -69,6 +69,7 @@ function mapOutputsToAssetItems({
|
||||
items.push({
|
||||
id: `${jobId}-${outputKey}`,
|
||||
name: output.filename,
|
||||
display_name: output.display_name,
|
||||
size: 0,
|
||||
created_at: createdAtValue,
|
||||
tags: ['output'],
|
||||
|
||||
@@ -317,8 +317,7 @@ export function useNodeReplacement() {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error', 'Error'),
|
||||
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes'),
|
||||
life: 5000
|
||||
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes')
|
||||
})
|
||||
return replacedTypes
|
||||
} finally {
|
||||
|
||||
@@ -23,7 +23,8 @@ const zPreviewOutput = z.object({
|
||||
subfolder: z.string(),
|
||||
type: resultItemType,
|
||||
nodeId: z.string(),
|
||||
mediaType: z.string()
|
||||
mediaType: z.string(),
|
||||
display_name: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@@ -83,8 +83,7 @@ describe('useSecrets', () => {
|
||||
expect(mockAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'Network error',
|
||||
life: 5000
|
||||
detail: 'Network error'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -130,8 +129,7 @@ describe('useSecrets', () => {
|
||||
expect(mockAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'Delete failed',
|
||||
life: 5000
|
||||
detail: 'Delete failed'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,16 +33,14 @@ export function useSecrets() {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: err.message,
|
||||
life: 5000
|
||||
detail: err.message
|
||||
})
|
||||
} else {
|
||||
console.error('Unexpected error fetching secrets:', err)
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: t('g.unknownError')
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
@@ -60,16 +58,14 @@ export function useSecrets() {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: err.message,
|
||||
life: 5000
|
||||
detail: err.message
|
||||
})
|
||||
} else {
|
||||
console.error('Unexpected error deleting secret:', err)
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: t('g.unknownError')
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -370,8 +370,7 @@ export const useWorkflowService = () => {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToSaveDraft'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedToSaveDraft')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +112,7 @@ export function useWorkflowPersistenceV2() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('toastMessages.failedToSaveDraft'),
|
||||
life: 3000
|
||||
detail: t('toastMessages.failedToSaveDraft')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -484,8 +484,7 @@ describe('ShareWorkflowDialogContent', () => {
|
||||
expect(mockToast.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Publish failed',
|
||||
life: 5000
|
||||
detail: 'Publish failed'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -352,8 +352,7 @@ const { isLoading: isSaving, execute: handleSave } = useAsyncState(
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('shareWorkflow.saveFailedTitle'),
|
||||
detail: t('shareWorkflow.saveFailedDescription'),
|
||||
life: 5000
|
||||
detail: t('shareWorkflow.saveFailedDescription')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -391,8 +390,7 @@ const {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.error'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.error')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +183,7 @@ async function handleCreate() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : t('g.error'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.error')
|
||||
})
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
|
||||
@@ -338,8 +338,7 @@ describe('useSharedWorkflowUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load shared workflow',
|
||||
life: 3000
|
||||
detail: 'Failed to load shared workflow'
|
||||
})
|
||||
expect(mockRouterReplace).toHaveBeenCalledWith({ query: {} })
|
||||
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
|
||||
|
||||
@@ -118,8 +118,7 @@ export function useSharedWorkflowUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('shareWorkflow.loadFailed'),
|
||||
life: 3000
|
||||
detail: t('shareWorkflow.loadFailed')
|
||||
})
|
||||
cleanupUrlParams()
|
||||
clearPreservedQuery(SHARE_NAMESPACE)
|
||||
@@ -148,8 +147,7 @@ export function useSharedWorkflowUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('shareWorkflow.loadFailed'),
|
||||
life: 5000
|
||||
detail: t('shareWorkflow.loadFailed')
|
||||
})
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
@@ -145,8 +145,7 @@ describe('useTemplateUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Template "invalid-template" not found',
|
||||
life: 3000
|
||||
detail: 'Template "invalid-template" not found'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,8 +238,7 @@ describe('useTemplateUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load template',
|
||||
life: 3000
|
||||
detail: 'Failed to load template'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -117,8 +117,7 @@ export function useTemplateUrlLoader() {
|
||||
summary: t('g.error'),
|
||||
detail: t('templateWorkflows.error.templateNotFound', {
|
||||
templateName: templateParam
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
})
|
||||
} else if (modeParam === 'linear') {
|
||||
// Set linear mode after successful template load
|
||||
@@ -132,8 +131,7 @@ export function useTemplateUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.errorLoadingTemplate'),
|
||||
life: 3000
|
||||
detail: t('g.errorLoadingTemplate')
|
||||
})
|
||||
} finally {
|
||||
cleanupUrlParams()
|
||||
|
||||
@@ -428,8 +428,7 @@ async function handleResubscribe() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isResubscribing.value = false
|
||||
|
||||
@@ -148,8 +148,7 @@ async function handleSubscribeClick(payload: {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Unable to subscribe',
|
||||
detail: 'This plan is not available',
|
||||
life: 5000
|
||||
detail: 'This plan is not available'
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -159,8 +158,7 @@ async function handleSubscribeClick(payload: {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Unable to subscribe',
|
||||
detail: response?.reason || 'This plan is not available',
|
||||
life: 5000
|
||||
detail: response?.reason || 'This plan is not available'
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -175,8 +173,7 @@ async function handleSubscribeClick(payload: {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isLoadingPreview.value = false
|
||||
@@ -236,8 +233,7 @@ async function handleAddCreditCard() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isSubscribing.value = false
|
||||
@@ -291,8 +287,7 @@ async function handleConfirmTransition() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isSubscribing.value = false
|
||||
@@ -316,8 +311,7 @@ async function handleResubscribe() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: message,
|
||||
life: 5000
|
||||
detail: message
|
||||
})
|
||||
} finally {
|
||||
isResubscribing.value = false
|
||||
|
||||
@@ -273,8 +273,7 @@ async function handleBuy() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('credits.topUp.purchaseError'),
|
||||
detail: t('credits.topUp.unknownError'),
|
||||
life: 5000
|
||||
detail: t('credits.topUp.unknownError')
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -285,8 +284,7 @@ async function handleBuy() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('credits.topUp.purchaseError'),
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
|
||||
life: 5000
|
||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -139,7 +139,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
const { switchWorkspace } = useWorkspaceSwitch()
|
||||
const { subscription } = useBillingContext()
|
||||
|
||||
const tierKeyMap: Record<string, string> = {
|
||||
@@ -226,7 +226,7 @@ function getTierLabel(workspace: AvailableWorkspace): string | null {
|
||||
}
|
||||
|
||||
async function handleSelectWorkspace(workspace: AvailableWorkspace) {
|
||||
const success = await switchWithConfirmation(workspace.id)
|
||||
const success = await switchWorkspace(workspace.id)
|
||||
if (success) {
|
||||
emit('select', workspace)
|
||||
}
|
||||
|
||||
@@ -102,8 +102,7 @@ async function onCreate() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToCreateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -79,8 +79,7 @@ async function onDelete() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToDeleteWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -94,8 +94,7 @@ async function onSave() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToUpdateWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -138,8 +138,7 @@ async function onCreateLink() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
|
||||
detail: error instanceof Error ? error.message : undefined,
|
||||
life: 3000
|
||||
detail: error instanceof Error ? error.message : undefined
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -161,8 +160,7 @@ async function onCopyLink() {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed'),
|
||||
life: 3000
|
||||
summary: t('workspacePanel.inviteMemberDialog.linkCopyFailed')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,7 @@ async function onLeave() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.toast.failedToLeaveWorkspace'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -73,8 +73,7 @@ async function onRemove() {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspacePanel.removeMemberDialog.error'),
|
||||
life: 3000
|
||||
summary: t('workspacePanel.removeMemberDialog.error')
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -69,8 +69,7 @@ async function onRevoke() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error instanceof Error ? error.message : undefined,
|
||||
life: 3000
|
||||
detail: error instanceof Error ? error.message : undefined
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -543,8 +543,7 @@ async function handleCopyInviteLink(invite: PendingInvite) {
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
life: 3000
|
||||
summary: t('g.error')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,18 @@ import { useWorkspaceSwitch } from '@/platform/workspace/composables/useWorkspac
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
const { switchWorkspace } = useWorkspaceSwitch()
|
||||
|
||||
function viewWorkspace(workspaceId: string) {
|
||||
void switchWithConfirmation(workspaceId)
|
||||
toast.removeGroup('invite-accepted')
|
||||
async function viewWorkspace(workspaceId: string) {
|
||||
const success = await switchWorkspace(workspaceId)
|
||||
if (success) {
|
||||
toast.removeGroup('invite-accepted')
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspace.switchFailed'),
|
||||
life: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -151,8 +151,7 @@ describe('useInviteUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Failed to Accept Invite',
|
||||
detail: 'Invalid invite',
|
||||
life: 5000
|
||||
detail: 'Invalid invite'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -211,8 +210,7 @@ describe('useInviteUrlLoader', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Failed to Accept Invite',
|
||||
detail: 'Invalid token',
|
||||
life: 5000
|
||||
detail: 'Invalid token'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -97,8 +97,7 @@ export function useInviteUrlLoader() {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('workspace.inviteFailed'),
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
||||
life: 5000
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
cleanupUrlParams()
|
||||
|
||||
@@ -20,32 +20,6 @@ vi.mock('pinia', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
const mockModifiedWorkflows = vi.hoisted(
|
||||
() => [] as Array<{ isModified: boolean }>
|
||||
)
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
get modifiedWorkflows() {
|
||||
return mockModifiedWorkflows
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
const mockConfirm = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
confirm: mockConfirm
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key: string) => key
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useWorkspaceSwitch', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -57,103 +31,37 @@ describe('useWorkspaceSwitch', () => {
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
joined_at: '2026-01-01T00:00:00Z'
|
||||
}
|
||||
mockModifiedWorkflows.length = 0
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('hasUnsavedChanges', () => {
|
||||
it('returns true when there are modified workflows', () => {
|
||||
mockModifiedWorkflows.push({ isModified: true })
|
||||
const { hasUnsavedChanges } = useWorkspaceSwitch()
|
||||
|
||||
expect(hasUnsavedChanges()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true when multiple workflows are modified', () => {
|
||||
mockModifiedWorkflows.push({ isModified: true }, { isModified: true })
|
||||
const { hasUnsavedChanges } = useWorkspaceSwitch()
|
||||
|
||||
expect(hasUnsavedChanges()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no workflows are modified', () => {
|
||||
mockModifiedWorkflows.length = 0
|
||||
const { hasUnsavedChanges } = useWorkspaceSwitch()
|
||||
|
||||
expect(hasUnsavedChanges()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('switchWithConfirmation', () => {
|
||||
describe('switchWorkspace', () => {
|
||||
it('returns true immediately if switching to the same workspace', async () => {
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
const { switchWorkspace } = useWorkspaceSwitch()
|
||||
|
||||
const result = await switchWithConfirmation('workspace-1')
|
||||
const result = await switchWorkspace('workspace-1')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockSwitchWorkspace).not.toHaveBeenCalled()
|
||||
expect(mockConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('switches directly without dialog when no unsaved changes', async () => {
|
||||
mockModifiedWorkflows.length = 0
|
||||
it('switches directly to the new workspace', async () => {
|
||||
mockSwitchWorkspace.mockResolvedValue(undefined)
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
const { switchWorkspace } = useWorkspaceSwitch()
|
||||
|
||||
const result = await switchWithConfirmation('workspace-2')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockConfirm).not.toHaveBeenCalled()
|
||||
expect(mockSwitchWorkspace).toHaveBeenCalledWith('workspace-2')
|
||||
})
|
||||
|
||||
it('shows confirmation dialog when there are unsaved changes', async () => {
|
||||
mockModifiedWorkflows.push({ isModified: true })
|
||||
mockConfirm.mockResolvedValue(true)
|
||||
mockSwitchWorkspace.mockResolvedValue(undefined)
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
|
||||
await switchWithConfirmation('workspace-2')
|
||||
|
||||
expect(mockConfirm).toHaveBeenCalledWith({
|
||||
title: 'workspace.unsavedChanges.title',
|
||||
message: 'workspace.unsavedChanges.message',
|
||||
type: 'dirtyClose'
|
||||
})
|
||||
})
|
||||
|
||||
it('returns false if user cancels the confirmation dialog', async () => {
|
||||
mockModifiedWorkflows.push({ isModified: true })
|
||||
mockConfirm.mockResolvedValue(false)
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
|
||||
const result = await switchWithConfirmation('workspace-2')
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(mockSwitchWorkspace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls switchWorkspace after user confirms', async () => {
|
||||
mockModifiedWorkflows.push({ isModified: true })
|
||||
mockConfirm.mockResolvedValue(true)
|
||||
mockSwitchWorkspace.mockResolvedValue(undefined)
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
|
||||
const result = await switchWithConfirmation('workspace-2')
|
||||
const result = await switchWorkspace('workspace-2')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockSwitchWorkspace).toHaveBeenCalledWith('workspace-2')
|
||||
})
|
||||
|
||||
it('returns false if switchWorkspace throws an error', async () => {
|
||||
mockModifiedWorkflows.length = 0
|
||||
mockSwitchWorkspace.mockRejectedValue(new Error('Switch failed'))
|
||||
const { switchWithConfirmation } = useWorkspaceSwitch()
|
||||
const { switchWorkspace } = useWorkspaceSwitch()
|
||||
|
||||
const result = await switchWithConfirmation('workspace-2')
|
||||
const result = await switchWorkspace('workspace-2')
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
@@ -1,41 +1,18 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
export function useWorkspaceSwitch() {
|
||||
const { t } = useI18n()
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const { activeWorkspace } = storeToRefs(workspaceStore)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
|
||||
function hasUnsavedChanges(): boolean {
|
||||
return workflowStore.modifiedWorkflows.length > 0
|
||||
}
|
||||
|
||||
async function switchWithConfirmation(workspaceId: string): Promise<boolean> {
|
||||
async function switchWorkspace(workspaceId: string): Promise<boolean> {
|
||||
if (activeWorkspace.value?.id === workspaceId) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (hasUnsavedChanges()) {
|
||||
const confirmed = await dialogService.confirm({
|
||||
title: t('workspace.unsavedChanges.title'),
|
||||
message: t('workspace.unsavedChanges.message'),
|
||||
type: 'dirtyClose'
|
||||
})
|
||||
|
||||
if (!confirmed) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await workspaceStore.switchWorkspace(workspaceId)
|
||||
// Note: switchWorkspace triggers page reload internally
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
@@ -43,7 +20,6 @@ export function useWorkspaceSwitch() {
|
||||
}
|
||||
|
||||
return {
|
||||
hasUnsavedChanges,
|
||||
switchWithConfirmation
|
||||
switchWorkspace
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,8 +219,7 @@ describe('billingOperationStore', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.subscriptionFailed',
|
||||
detail: errorMessage,
|
||||
life: 5000
|
||||
detail: errorMessage
|
||||
})
|
||||
})
|
||||
|
||||
@@ -239,8 +238,7 @@ describe('billingOperationStore', () => {
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.topupFailed',
|
||||
detail: undefined,
|
||||
life: 5000
|
||||
detail: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -267,8 +265,7 @@ describe('billingOperationStore', () => {
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.subscriptionTimeout',
|
||||
life: 5000
|
||||
summary: 'billingOperation.subscriptionTimeout'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -287,8 +284,7 @@ describe('billingOperationStore', () => {
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'billingOperation.topupTimeout',
|
||||
life: 5000
|
||||
summary: 'billingOperation.topupTimeout'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -173,8 +173,7 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: defaultMessage,
|
||||
detail: errorMessage ?? undefined,
|
||||
life: 5000
|
||||
detail: errorMessage ?? undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,8 +191,7 @@ export const useBillingOperationStore = defineStore('billingOperation', () => {
|
||||
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: message,
|
||||
life: 5000
|
||||
summary: message
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function togglePromotion() {
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-auto absolute size-full rounded-2xl ring-5 ring-warning-background/50',
|
||||
'pointer-events-auto absolute z-1 size-full rounded-2xl ring-5 ring-warning-background/50',
|
||||
isPromoted && 'ring-warning-background'
|
||||
)
|
||||
"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, useTimeout } from '@vueuse/core'
|
||||
import { partition, remove, takeWhile } from 'es-toolkit'
|
||||
import { remove, takeWhile } from 'es-toolkit'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -19,6 +19,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
||||
import PartnerNodesList from '@/renderer/extensions/linearMode/PartnerNodesList.vue'
|
||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -119,20 +120,6 @@ function nodeToNodeData(node: LGraphNode) {
|
||||
onDragOver: node.onDragOver
|
||||
}
|
||||
}
|
||||
const partitionedNodes = computed(() => {
|
||||
const parts = partition(
|
||||
graphNodes.value
|
||||
.filter((node) => node.mode === 0 && node.widgets?.length)
|
||||
.map(nodeToNodeData)
|
||||
.reverse(),
|
||||
(node) => ['MarkdownNote', 'Note'].includes(node.type)
|
||||
)
|
||||
for (const noteNode of parts[0]) {
|
||||
for (const widget of noteNode.widgets ?? [])
|
||||
widget.options = { ...widget.options, read_only: true }
|
||||
}
|
||||
return parts
|
||||
})
|
||||
|
||||
//TODO: refactor out of this file.
|
||||
//code length is small, but changes should propagate
|
||||
@@ -180,34 +167,6 @@ defineExpose({ runButtonClick })
|
||||
v-text="workflowStore.activeWorkflow?.filename"
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<Popover
|
||||
v-if="partitionedNodes[0].length"
|
||||
align="end"
|
||||
class="z-100 max-h-(--reka-popover-content-available-height) overflow-x-clip overflow-y-auto"
|
||||
side="bottom"
|
||||
:side-offset="-8"
|
||||
>
|
||||
<template #button>
|
||||
<Button variant="muted-textonly">
|
||||
<i class="icon-[lucide--info]" />
|
||||
</Button>
|
||||
</template>
|
||||
<div>
|
||||
<template
|
||||
v-for="(nodeData, index) in partitionedNodes[0]"
|
||||
:key="nodeData.id"
|
||||
>
|
||||
<div
|
||||
v-if="index !== 0"
|
||||
class="w-full border-t border-border-subtle"
|
||||
/>
|
||||
<NodeWidgets
|
||||
:node-data
|
||||
class="max-w-100 gap-y-3 rounded-lg py-3 *:has-[textarea]:h-50 **:[.col-span-2]:grid-cols-1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</Popover>
|
||||
<Button v-if="false"> {{ t('menuLabels.publish') }} </Button>
|
||||
</section>
|
||||
<div
|
||||
@@ -218,9 +177,7 @@ defineExpose({ runButtonClick })
|
||||
class="grow overflow-y-auto contain-size"
|
||||
>
|
||||
<template
|
||||
v-for="(nodeData, index) of appModeStore.selectedInputs.length
|
||||
? mappedSelections
|
||||
: partitionedNodes[0]"
|
||||
v-for="(nodeData, index) of mappedSelections"
|
||||
:key="nodeData.id"
|
||||
>
|
||||
<div
|
||||
@@ -274,6 +231,7 @@ defineExpose({ runButtonClick })
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
<PartnerNodesList v-if="!mobile" />
|
||||
<section
|
||||
v-if="mobile"
|
||||
data-testid="linear-run-button"
|
||||
@@ -284,6 +242,7 @@ defineExpose({ runButtonClick })
|
||||
class="mt-4 w-full"
|
||||
/>
|
||||
<div v-else class="mt-4 flex">
|
||||
<PartnerNodesList mobile />
|
||||
<Popover side="top" @open-auto-focus.prevent>
|
||||
<template #button>
|
||||
<Button size="lg" class="-mr-3 pr-7">
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useInfiniteScroll,
|
||||
useResizeObserver
|
||||
} from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import {
|
||||
computed,
|
||||
@@ -26,11 +27,13 @@ import type {
|
||||
import OutputPreviewItem from '@/renderer/extensions/linearMode/OutputPreviewItem.vue'
|
||||
import { useOutputHistory } from '@/renderer/extensions/linearMode/useOutputHistory'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { outputs, allOutputs, selectFirstHistory, mayBeActiveWorkflowPending } =
|
||||
useOutputHistory()
|
||||
const { hasOutputs } = storeToRefs(useAppModeStore())
|
||||
const queueStore = useQueueStore()
|
||||
const store = useLinearOutputStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -156,8 +159,10 @@ watch(
|
||||
const inProgress = store.activeWorkflowInProgressItems
|
||||
if (inProgress.length > 0) {
|
||||
store.selectAsLatest(`slot:${inProgress[0].id}`)
|
||||
} else {
|
||||
} else if (hasOutputs.value) {
|
||||
selectFirstHistory()
|
||||
} else {
|
||||
store.selectAsLatest(null)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -180,13 +185,13 @@ watch(
|
||||
: undefined
|
||||
|
||||
if (!sv || sv.kind !== 'history') {
|
||||
selectFirstHistory()
|
||||
if (hasOutputs.value) selectFirstHistory()
|
||||
return
|
||||
}
|
||||
|
||||
const wasFirst = sv.assetId === oldAssets[0]?.id
|
||||
if (wasFirst || !newAssets.some((a) => a.id === sv.assetId)) {
|
||||
selectFirstHistory()
|
||||
if (hasOutputs.value) selectFirstHistory()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
14
src/renderer/extensions/linearMode/PartnerNodeItem.vue
Normal file
14
src/renderer/extensions/linearMode/PartnerNodeItem.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ title: string; price: string }>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="not-last:mb-4">
|
||||
<div class="text-muted-foreground" v-text="title" />
|
||||
<span
|
||||
class="mt-2 flex h-5 max-w-max items-center rounded-full bg-component-node-widget-background p-2 py-3"
|
||||
>
|
||||
<i class="mr-1 icon-[lucide--component] h-4 bg-amber-400" />
|
||||
{{ price }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
85
src/renderer/extensions/linearMode/PartnerNodesList.vue
Normal file
85
src/renderer/extensions/linearMode/PartnerNodesList.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
CollapsibleContent,
|
||||
CollapsibleRoot,
|
||||
CollapsibleTrigger
|
||||
} from 'reka-ui'
|
||||
import { computed, toValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
||||
import PartnerNodeItem from '@/renderer/extensions/linearMode/PartnerNodeItem.vue'
|
||||
import { trackNodePrice } from '@/renderer/extensions/vueNodes/composables/usePartitionedBadges'
|
||||
import { app } from '@/scripts/app'
|
||||
import { mapAllNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
defineProps<{ mobile?: boolean }>()
|
||||
|
||||
const { isCreditsBadge } = usePriceBadge()
|
||||
const { t } = useI18n()
|
||||
|
||||
const creditsBadges = computed(() =>
|
||||
mapAllNodes(app.graph, (node) => {
|
||||
if (node.isSubgraphNode()) return
|
||||
|
||||
const priceBadge = node.badges.find(isCreditsBadge)
|
||||
if (!priceBadge) return
|
||||
|
||||
trackNodePrice(node)
|
||||
return [node.title, toValue(priceBadge).text, node.id] as const
|
||||
})
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<Popover v-if="mobile && creditsBadges.length" side="top">
|
||||
<template #button>
|
||||
<Button class="mr-2 size-10">
|
||||
<i class="icon-[comfy--credits] bg-amber-400" />
|
||||
</Button>
|
||||
</template>
|
||||
<section
|
||||
class="max-h-(--reka-popover-content-available-height) overflow-y-auto"
|
||||
>
|
||||
<PartnerNodeItem
|
||||
v-for="[title, price, key] in creditsBadges"
|
||||
:key
|
||||
:title
|
||||
:price
|
||||
/>
|
||||
</section>
|
||||
</Popover>
|
||||
<div v-else-if="creditsBadges.length === 1">
|
||||
<PartnerNodeItem
|
||||
v-for="[title, price, key] in creditsBadges"
|
||||
:key
|
||||
:title
|
||||
:price
|
||||
class="border-t border-border-subtle pt-2"
|
||||
/>
|
||||
</div>
|
||||
<CollapsibleRoot
|
||||
v-else-if="creditsBadges.length"
|
||||
v-slot="{ open }"
|
||||
class="flex max-h-1/2 w-full flex-col"
|
||||
>
|
||||
<div class="mb-1 w-full border-b border-border-subtle" />
|
||||
<CollapsibleTrigger as-child>
|
||||
<Button variant="textonly" class="w-full text-sm">
|
||||
<i class="icon-[comfy--credits] size-4 bg-amber-400" />
|
||||
{{ t('linearMode.hasCreditCost') }}
|
||||
<i v-if="open" class="ml-auto icon-[lucide--chevron-up]" />
|
||||
<i v-else class="ml-auto icon-[lucide--chevron-down]" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent class="overflow-y-auto">
|
||||
<PartnerNodeItem
|
||||
v-for="[title, price, key] in creditsBadges"
|
||||
:key
|
||||
:title
|
||||
:price
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
@@ -219,6 +219,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('returns outputs from metadata allOutputs when count matches', () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
const results = [makeResult('a.png'), makeResult('b.png')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -255,7 +256,7 @@ describe(useOutputHistory, () => {
|
||||
expect(outputs[0].filename).toBe('b.png')
|
||||
})
|
||||
|
||||
it('returns all outputs when no output nodes are selected', () => {
|
||||
it('returns empty when no output nodes are selected', () => {
|
||||
const results = [makeResult('a.png', '1'), makeResult('b.png', '2')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -265,7 +266,7 @@ describe(useOutputHistory, () => {
|
||||
const { allOutputs } = useOutputHistory()
|
||||
const outputs = allOutputs(asset)
|
||||
|
||||
expect(outputs).toHaveLength(2)
|
||||
expect(outputs).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns consistent filtered outputs across repeated calls', () => {
|
||||
@@ -288,6 +289,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('returns in-progress outputs for pending resolve jobs', () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
pendingResolveRef.value = new Set(['job-1'])
|
||||
inProgressItemsRef.value = [
|
||||
{
|
||||
@@ -314,6 +316,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('fetches full job detail for multi-output jobs', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
jobDetailResults.set('job-1', {
|
||||
outputs: {
|
||||
'1': {
|
||||
@@ -342,6 +345,7 @@ describe(useOutputHistory, () => {
|
||||
|
||||
describe('watchEffect resolve loop', () => {
|
||||
it('resolves pending jobs when history outputs load', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
const results = [makeResult('a.png')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
@@ -360,6 +364,7 @@ describe(useOutputHistory, () => {
|
||||
})
|
||||
|
||||
it('does not select first history when a selection exists', async () => {
|
||||
useAppModeStore().selectedOutputs.push('1')
|
||||
const results = [makeResult('a.png')]
|
||||
const asset = makeAsset('a1', 'job-1', {
|
||||
allOutputs: results,
|
||||
|
||||
@@ -65,7 +65,7 @@ export function useOutputHistory(): {
|
||||
|
||||
function filterByOutputNodes(items: ResultItemImpl[]): ResultItemImpl[] {
|
||||
const nodeIds = appModeStore.selectedOutputs
|
||||
if (!nodeIds.length) return items
|
||||
if (!nodeIds.length) return []
|
||||
return items.filter((r) =>
|
||||
nodeIds.some((id) => String(id) === String(r.nodeId))
|
||||
)
|
||||
|
||||
@@ -203,7 +203,6 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadVideo'),
|
||||
life: 3000,
|
||||
group: 'video-preview'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -233,7 +233,6 @@ const handleDownload = () => {
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: t('g.failedToDownloadImage'),
|
||||
life: 3000,
|
||||
group: 'image-preview'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
v-if="
|
||||
lgraphNode?.constructor?.nodeData?.output_node &&
|
||||
isSelectOutputsMode &&
|
||||
nodeData.mode === LGraphEventMode.ALWAYS
|
||||
nodeData.mode === LGraphEventMode.ALWAYS &&
|
||||
!nodeData.hasErrors
|
||||
"
|
||||
:id="nodeData.id"
|
||||
/>
|
||||
@@ -91,10 +92,6 @@
|
||||
backgroundColor: applyLightThemeColor(nodeData?.color)
|
||||
}"
|
||||
>
|
||||
<AppOutput
|
||||
v-if="lgraphNode?.constructor?.nodeData?.output_node && isSelectMode"
|
||||
:id="nodeData.id"
|
||||
/>
|
||||
<div
|
||||
v-if="displayHeader"
|
||||
class="relative flex flex-col items-center justify-center"
|
||||
|
||||
@@ -109,6 +109,7 @@ import {
|
||||
shouldExpand,
|
||||
shouldRenderAsVue
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { nodeTypeValidForApp } from '@/stores/appModeStore'
|
||||
import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
@@ -160,7 +161,11 @@ onErrorCaptured((error) => {
|
||||
})
|
||||
|
||||
const canSelectInputs = computed(
|
||||
() => isSelectInputsMode.value && nodeData?.mode === LGraphEventMode.ALWAYS
|
||||
() =>
|
||||
isSelectInputsMode.value &&
|
||||
nodeData?.mode === LGraphEventMode.ALWAYS &&
|
||||
nodeTypeValidForApp(nodeData.type) &&
|
||||
!nodeData.hasErrors
|
||||
)
|
||||
const nodeType = computed(() => nodeData?.type || '')
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
@@ -4,6 +4,8 @@ import { computed, toValue } from 'vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useNodePricing } from '@/composables/node/useNodePricing'
|
||||
import { usePriceBadge } from '@/composables/node/usePriceBadge'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeBadgeProps } from '@/renderer/extensions/vueNodes/components/NodeBadge.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -17,6 +19,56 @@ function splitAroundFirstSpace(text: string): [string, string | undefined] {
|
||||
return [text.slice(0, index), text.slice(index + 1)]
|
||||
}
|
||||
|
||||
type TrackableNode = {
|
||||
id: NodeId
|
||||
type: string
|
||||
inputs?: INodeInputSlot[]
|
||||
}
|
||||
//TODO deduplicate reactivity tracking once more thoroughly tested
|
||||
export function trackNodePrice(node: TrackableNode) {
|
||||
const {
|
||||
getRelevantWidgetNames,
|
||||
hasDynamicPricing,
|
||||
getInputGroupPrefixes,
|
||||
getInputNames,
|
||||
getNodeRevisionRef
|
||||
} = useNodePricing()
|
||||
// Access per-node revision ref to establish dependency (each node has its own ref)
|
||||
void getNodeRevisionRef(node.id).value
|
||||
|
||||
if (!hasDynamicPricing(node.type)) return
|
||||
|
||||
// Access only the widget values that affect pricing (from widgetValueStore)
|
||||
const relevantNames = getRelevantWidgetNames(node.type)
|
||||
const widgetStore = useWidgetValueStore()
|
||||
const graphId = app.canvas?.graph?.rootGraph.id
|
||||
if (relevantNames.length > 0 && node.id != null) {
|
||||
for (const name of relevantNames) {
|
||||
// Access value from store to create reactive dependency
|
||||
if (!graphId) continue
|
||||
void widgetStore.getWidget(graphId, node.id, name)?.value
|
||||
}
|
||||
}
|
||||
// Access input connections for regular inputs
|
||||
const inputNames = getInputNames(node.type)
|
||||
if (inputNames.length > 0) {
|
||||
node?.inputs?.forEach((inp) => {
|
||||
if (inp.name && inputNames.includes(inp.name)) {
|
||||
void inp.link // Access link to create reactive dependency
|
||||
}
|
||||
})
|
||||
}
|
||||
// Access input connections for input_groups (e.g., autogrow inputs)
|
||||
const groupPrefixes = getInputGroupPrefixes(node.type)
|
||||
if (groupPrefixes.length > 0) {
|
||||
node?.inputs?.forEach((inp) => {
|
||||
if (groupPrefixes.some((prefix) => inp.name?.startsWith(prefix + '.'))) {
|
||||
void inp.link // Access link to create reactive dependency
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function usePartitionedBadges(nodeData: VueNodeData) {
|
||||
// Use per-node pricing revision to re-compute badges only when this node's pricing updates
|
||||
const {
|
||||
|
||||
@@ -208,8 +208,7 @@ const handleDownload = () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('g.failedToDownloadFile'),
|
||||
life: 3000
|
||||
detail: t('g.failedToDownloadFile')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user