Compare commits
24 Commits
fix/codera
...
drjkl/one-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953374b9de | ||
|
|
725a0a2b89 | ||
|
|
8a5bcde168 | ||
|
|
83ffaf30c8 | ||
|
|
2875f897dc | ||
|
|
ec129de63d | ||
|
|
1687ca93b3 | ||
|
|
5bb742ac3a | ||
|
|
ca2d61f393 | ||
|
|
750a2d23e0 | ||
|
|
6d90bf3537 | ||
|
|
1ada6dbfc6 | ||
|
|
f02adf84eb | ||
|
|
1058b7d12d | ||
|
|
8bfd93963f | ||
|
|
3366079f59 | ||
|
|
c4dabb8f98 | ||
|
|
0b73285ca1 | ||
|
|
7a01be388f | ||
|
|
3ddff9f7b6 | ||
|
|
4ff14b5eb9 | ||
|
|
bae1081a08 | ||
|
|
55b8236c8d | ||
|
|
5e17bbbf85 |
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Update electron types
|
- name: Update electron types
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
25
.github/workflows/cloud-dispatch-build.yaml
vendored
@@ -13,6 +13,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- 'cloud/*'
|
- 'cloud/*'
|
||||||
- 'main'
|
- 'main'
|
||||||
|
pull_request:
|
||||||
|
types: [labeled]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -23,16 +25,31 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dispatch:
|
dispatch:
|
||||||
# Fork guard: prevent forks from dispatching to the cloud repo
|
# Fork guard: prevent forks from dispatching to the cloud repo.
|
||||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
|
# For pull_request events, only dispatch when the 'preview' label is added.
|
||||||
|
if: >
|
||||||
|
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||||
|
(github.event_name != 'pull_request' ||
|
||||||
|
github.event.label.name == 'preview')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Build client payload
|
- name: Build client payload
|
||||||
id: payload
|
id: payload
|
||||||
|
env:
|
||||||
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||||
run: |
|
run: |
|
||||||
|
if [ "${EVENT_NAME}" = "pull_request" ]; then
|
||||||
|
REF="${PR_HEAD_SHA}"
|
||||||
|
BRANCH="${PR_HEAD_REF}"
|
||||||
|
else
|
||||||
|
REF="${GITHUB_SHA}"
|
||||||
|
BRANCH="${GITHUB_REF_NAME}"
|
||||||
|
fi
|
||||||
payload="$(jq -nc \
|
payload="$(jq -nc \
|
||||||
--arg ref "${GITHUB_SHA}" \
|
--arg ref "${REF}" \
|
||||||
--arg branch "${GITHUB_REF_NAME}" \
|
--arg branch "${BRANCH}" \
|
||||||
'{ref: $ref, branch: $branch}')"
|
'{ref: $ref, branch: $branch}')"
|
||||||
echo "json=${payload}" >> "${GITHUB_OUTPUT}"
|
echo "json=${payload}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/pr-claude-review.yaml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies for analysis tools
|
- name: Install dependencies for analysis tools
|
||||||
|
|||||||
2
.github/workflows/pr-perf-report.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Download PR metadata
|
- name: Download PR metadata
|
||||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Read desktop-ui version
|
- name: Read desktop-ui version
|
||||||
id: get_version
|
id: get_version
|
||||||
|
|||||||
2
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -91,7 +91,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
registry-url: https://registry.npmjs.org
|
registry-url: https://registry.npmjs.org
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version-file: 'frontend/.nvmrc'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
2
.github/workflows/release-branch-create.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Check version bump type
|
- name: Check version bump type
|
||||||
id: check_version
|
id: check_version
|
||||||
|
|||||||
2
.github/workflows/release-draft-create.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
version: 10
|
version: 10
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Get current version
|
- name: Get current version
|
||||||
|
|||||||
2
.github/workflows/release-npm-types.yaml
vendored
@@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
registry-url: https://registry.npmjs.org
|
registry-url: https://registry.npmjs.org
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/release-pypi-dev.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
version: 10
|
version: 10
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'lts/*'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Get current version
|
- name: Get current version
|
||||||
|
|||||||
2
.github/workflows/release-version-bump.yaml
vendored
@@ -149,7 +149,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump-version
|
id: bump-version
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '24.x'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Bump desktop-ui version
|
- name: Bump desktop-ui version
|
||||||
|
|||||||
2
.github/workflows/weekly-docs-check.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies for analysis tools
|
- name: Install dependencies for analysis tools
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
|
|||||||
### Prerequisites & Technology Stack
|
### Prerequisites & Technology Stack
|
||||||
|
|
||||||
- **Required Software**:
|
- **Required Software**:
|
||||||
- Node.js (v24) and pnpm
|
- Node.js (see `.nvmrc`, currently v24) and pnpm
|
||||||
- Git for version control
|
- Git for version control
|
||||||
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)
|
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
|
||||||
ref="rootEl"
|
<div class="p-terminal size-full rounded-none p-2">
|
||||||
class="relative overflow-hidden h-full w-full bg-neutral-900"
|
<div ref="terminalEl" class="terminal-host h-full" />
|
||||||
>
|
|
||||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
|
||||||
<div ref="terminalEl" class="h-full terminal-host" />
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip.left="{
|
v-tooltip.left="{
|
||||||
@@ -16,7 +13,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
:class="
|
:class="
|
||||||
cn('absolute top-2 right-8 transition-opacity', {
|
cn('absolute top-2 right-8 transition-opacity', {
|
||||||
'opacity-0 pointer-events-none select-none': !isHovered
|
'pointer-events-none opacity-0 select-none': !isHovered
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
:aria-label="tooltipText"
|
:aria-label="tooltipText"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
|
<div class="mx-auto flex w-full max-w-3xl flex-col gap-8 select-none">
|
||||||
<!-- Installation Path Section -->
|
<!-- Installation Path Section -->
|
||||||
<div class="grow flex flex-col gap-6 text-neutral-300">
|
<div class="flex grow flex-col gap-6 text-neutral-300">
|
||||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
|
||||||
{{ $t('install.locationPicker.title') }}
|
{{ $t('install.locationPicker.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="text-center text-neutral-400 px-12">
|
<p class="px-12 text-center text-neutral-400">
|
||||||
{{ $t('install.locationPicker.subtitle') }}
|
{{ $t('install.locationPicker.subtitle') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<InputText
|
<InputText
|
||||||
v-model="installPath"
|
v-model="installPath"
|
||||||
:placeholder="$t('install.locationPicker.pathPlaceholder')"
|
:placeholder="$t('install.locationPicker.pathPlaceholder')"
|
||||||
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
|
class="flex-1 border-neutral-700 bg-neutral-800/50 text-neutral-200 placeholder:text-neutral-500"
|
||||||
:class="{ 'p-invalid': pathError }"
|
:class="{ 'p-invalid': pathError }"
|
||||||
@update:model-value="validatePath"
|
@update:model-value="validatePath"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<Button
|
<Button
|
||||||
icon="pi pi-folder-open"
|
icon="pi pi-folder-open"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
class="bg-neutral-700 hover:bg-neutral-600 border-0"
|
class="border-0 bg-neutral-700 hover:bg-neutral-600"
|
||||||
@click="browsePath"
|
@click="browsePath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<Message
|
<Message
|
||||||
v-if="pathError"
|
v-if="pathError"
|
||||||
severity="error"
|
severity="error"
|
||||||
class="whitespace-pre-line w-full"
|
class="w-full whitespace-pre-line"
|
||||||
>
|
>
|
||||||
{{ pathError }}
|
{{ pathError }}
|
||||||
</Message>
|
</Message>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="task.headerImg"
|
v-if="task.headerImg"
|
||||||
:src="task.headerImg"
|
:src="task.headerImg"
|
||||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
class="size-full object-contain px-4 pt-4 opacity-25"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #title>
|
<template #title>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
<i
|
<i
|
||||||
v-if="!isLoading && runner.state === 'OK'"
|
v-if="!isLoading && runner.state === 'OK'"
|
||||||
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 col-span-full row-span-full z-10 text-[4rem] text-green-500 opacity-100 transition-opacity group-hover/task-card:opacity-20 [text-shadow:0.25rem_0_0.5rem_black]"
|
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 z-10 col-span-full row-span-full text-[4rem] text-green-500 opacity-100 transition-opacity [text-shadow:0.25rem_0_0.5rem_black] group-hover/task-card:opacity-20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<template v-if="filter.tasks.length === 0">
|
<template v-if="filter.tasks.length === 0">
|
||||||
<!-- Empty filter -->
|
<!-- Empty filter -->
|
||||||
<Divider />
|
<Divider />
|
||||||
<p class="text-neutral-400 w-full text-center">
|
<p class="w-full text-center text-neutral-400">
|
||||||
{{ $t('maintenance.allOk') }}
|
{{ $t('maintenance.allOk') }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<!-- Display: Cards -->
|
<!-- Display: Cards -->
|
||||||
<template v-else>
|
<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
|
<TaskCard
|
||||||
v-for="task in filter.tasks"
|
v-for="task in filter.tasks"
|
||||||
:key="task.id"
|
:key="task.id"
|
||||||
@@ -45,7 +45,8 @@ import { useConfirm, useToast } from 'primevue'
|
|||||||
import ConfirmPopup from 'primevue/confirmpopup'
|
import ConfirmPopup from 'primevue/confirmpopup'
|
||||||
import Divider from 'primevue/divider'
|
import Divider from 'primevue/divider'
|
||||||
|
|
||||||
import { t } from '@/i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||||
import type {
|
import type {
|
||||||
MaintenanceFilter,
|
MaintenanceFilter,
|
||||||
@@ -55,6 +56,7 @@ import type {
|
|||||||
import TaskCard from './TaskCard.vue'
|
import TaskCard from './TaskCard.vue'
|
||||||
import TaskListItem from './TaskListItem.vue'
|
import TaskListItem from './TaskListItem.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const taskStore = useMaintenanceTaskStore()
|
const taskStore = useMaintenanceTaskStore()
|
||||||
@@ -80,8 +82,7 @@ const executeTask = async (task: MaintenanceTask) => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('maintenance.error.toastTitle'),
|
summary: t('maintenance.error.toastTitle'),
|
||||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
detail: message ?? t('maintenance.error.defaultDescription')
|
||||||
life: 10_000
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
|
<div class="flex size-full flex-col justify-between rounded-lg p-6">
|
||||||
<h1 class="font-inter font-semibold text-xl m-0 italic">
|
<h1 class="m-0 font-inter text-xl font-semibold italic">
|
||||||
{{ $t(`desktopDialogs.${id}.title`, title) }}
|
{{ $t(`desktopDialogs.${id}.title`, title) }}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="whitespace-pre-wrap">
|
<p class="whitespace-pre-wrap">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<div
|
<div
|
||||||
class="h-screen w-screen grid items-center justify-around overflow-y-auto"
|
class="grid h-screen w-screen items-center justify-around overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div class="relative m-8 text-center">
|
<div class="relative m-8 text-center">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
<span>{{ $t('desktopUpdate.description') }}</span>
|
<span>{{ $t('desktopUpdate.description') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
<ProgressSpinner class="m-8 size-48" />
|
||||||
|
|
||||||
<!-- Console button -->
|
<!-- Console button -->
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<!-- Fixed height container with flexbox layout for proper content management -->
|
<!-- Fixed height container with flexbox layout for proper content management -->
|
||||||
<div class="w-full h-full flex flex-col">
|
<div class="flex size-full flex-col">
|
||||||
<Stepper
|
<Stepper
|
||||||
v-model:value="currentStep"
|
v-model:value="currentStep"
|
||||||
class="flex flex-col h-full"
|
class="flex h-full flex-col"
|
||||||
@update:value="handleStepChange"
|
@update:value="handleStepChange"
|
||||||
>
|
>
|
||||||
<!-- Main content area that grows to fill available space -->
|
<!-- Main content area that grows to fill available space -->
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<!-- Install footer with navigation -->
|
<!-- Install footer with navigation -->
|
||||||
<InstallFooter
|
<InstallFooter
|
||||||
class="w-full max-w-2xl my-6 mx-auto"
|
class="mx-auto my-6 w-full max-w-2xl"
|
||||||
:current-step
|
:current-step
|
||||||
:can-proceed
|
:can-proceed
|
||||||
:disable-location-step="noGpu"
|
:disable-location-step="noGpu"
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<div
|
<div
|
||||||
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme overflow-y-auto"
|
class="dark-theme grid h-screen min-h-full w-screen min-w-full justify-around overflow-y-auto bg-neutral-900 font-sans text-neutral-300"
|
||||||
>
|
>
|
||||||
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
|
<div class="relative m-8 w-screen max-w-(--breakpoint-sm)">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<h1 class="backspan pi-wrench text-4xl font-bold">
|
<h1 class="backspan pi-wrench text-4xl font-bold">
|
||||||
{{ t('maintenance.title') }}
|
{{ t('maintenance.title') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="w-full flex flex-wrap gap-4 items-center">
|
<div class="flex w-full flex-wrap items-center gap-4">
|
||||||
<span class="grow">
|
<span class="grow">
|
||||||
{{ t('maintenance.status') }}:
|
{{ t('maintenance.status') }}:
|
||||||
<StatusTag :refreshing="isRefreshing" :error="anyErrors" />
|
<StatusTag :refreshing="isRefreshing" :error="anyErrors" />
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 items-center">
|
<div class="flex items-center gap-4">
|
||||||
<SelectButton
|
<SelectButton
|
||||||
v-model="displayAsList"
|
v-model="displayAsList"
|
||||||
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
|
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
:value="t('icon.exclamation-triangle')"
|
:value="t('icon.exclamation-triangle')"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<strong class="block mb-1">
|
<strong class="mb-1 block">
|
||||||
{{ t('maintenance.unsafeMigration.title') }}
|
{{ t('maintenance.unsafeMigration.title') }}
|
||||||
</strong>
|
</strong>
|
||||||
<span class="block mb-1">
|
<span class="mb-1 block">
|
||||||
{{ unsafeReasonText }}
|
{{ unsafeReasonText }}
|
||||||
</span>
|
</span>
|
||||||
<span class="block text-sm text-neutral-400">
|
<span class="block text-sm text-neutral-400">
|
||||||
@@ -71,13 +71,13 @@
|
|||||||
|
|
||||||
<!-- Tasks -->
|
<!-- Tasks -->
|
||||||
<TaskListPanel
|
<TaskListPanel
|
||||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
class="border-x-0 border-y border-solid border-neutral-700"
|
||||||
:filter
|
:filter
|
||||||
:display-as-list
|
:display-as-list
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex justify-between gap-4 flex-row">
|
<div class="flex flex-row justify-between gap-4">
|
||||||
<Button
|
<Button
|
||||||
:label="t('maintenance.consoleLogs')"
|
:label="t('maintenance.consoleLogs')"
|
||||||
icon="pi pi-desktop"
|
icon="pi pi-desktop"
|
||||||
@@ -189,8 +189,7 @@ const completeValidation = async () => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('g.error'),
|
summary: t('g.error'),
|
||||||
detail: t('maintenance.error.cannotContinue'),
|
detail: t('maintenance.error.cannotContinue')
|
||||||
life: 5_000
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseViewTemplate dark hide-language-selector>
|
<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
|
<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">
|
<h2 class="text-3xl font-semibold text-neutral-100">
|
||||||
{{ $t('install.helpImprove') }}
|
{{ $t('install.helpImprove') }}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<a
|
<a
|
||||||
href="https://comfy.org/privacy"
|
href="https://comfy.org/privacy"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="text-blue-400 hover:text-blue-300 underline"
|
class="text-blue-400 underline hover:text-blue-300"
|
||||||
>
|
>
|
||||||
{{ $t('install.privacyPolicy') }} </a
|
{{ $t('install.privacyPolicy') }} </a
|
||||||
>.
|
>.
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pt-6 justify-end">
|
<div class="flex justify-end pt-6">
|
||||||
<Button
|
<Button
|
||||||
:label="$t('g.ok')"
|
:label="$t('g.ok')"
|
||||||
icon="pi pi-check"
|
icon="pi pi-check"
|
||||||
@@ -72,8 +72,7 @@ const updateConsent = async () => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('install.settings.errorUpdatingConsent'),
|
summary: t('install.settings.errorUpdatingConsent'),
|
||||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
detail: t('install.settings.errorUpdatingConsentDetail')
|
||||||
life: 3000
|
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
isUpdating.value = false
|
isUpdating.value = false
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="no-drag sad-text flex items-center">
|
<div class="no-drag sad-text flex items-center">
|
||||||
<div class="flex flex-col gap-8 p-8 min-w-110">
|
<div class="flex min-w-110 flex-col gap-8 p-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<h1 class="text-4xl font-bold text-red-500">
|
<h1 class="text-4xl font-bold text-red-500">
|
||||||
{{ $t('notSupported.title') }}
|
{{ $t('notSupported.title') }}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<p class="text-xl">
|
<p class="text-xl">
|
||||||
{{ $t('notSupported.message') }}
|
{{ $t('notSupported.message') }}
|
||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-inside space-y-1 text-neutral-800">
|
<ul class="list-inside list-disc space-y-1 text-neutral-800">
|
||||||
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
|
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
|
||||||
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
|
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<BaseViewTemplate dark>
|
<BaseViewTemplate dark>
|
||||||
<div class="relative min-h-screen">
|
<div class="relative min-h-screen">
|
||||||
<!-- Terminal Background Layer (always visible during loading) -->
|
<!-- Terminal Background Layer (always visible during loading) -->
|
||||||
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
|
<div v-if="!isError" class="fixed inset-0 z-0 overflow-hidden">
|
||||||
<div class="h-full w-full">
|
<div class="size-full">
|
||||||
<BaseTerminal @created="terminalCreated" />
|
<BaseTerminal @created="terminalCreated" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Semi-transparent overlay -->
|
<!-- Semi-transparent overlay -->
|
||||||
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
|
<div v-if="!isError" class="fixed inset-0 z-5 bg-neutral-900/80"></div>
|
||||||
|
|
||||||
<!-- Smooth radial gradient overlay -->
|
<!-- Smooth radial gradient overlay -->
|
||||||
<div
|
<div
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
<!-- Error Section (positioned at bottom) -->
|
<!-- Error Section (positioned at bottom) -->
|
||||||
<div
|
<div
|
||||||
v-if="isError"
|
v-if="isError"
|
||||||
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
|
class="absolute inset-x-0 bottom-20 flex flex-col items-center gap-4"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4 justify-center">
|
<div class="flex justify-center gap-4">
|
||||||
<Button
|
<Button
|
||||||
icon="pi pi-flag"
|
icon="pi pi-flag"
|
||||||
:label="$t('serverStart.reportIssue')"
|
:label="$t('serverStart.reportIssue')"
|
||||||
@@ -71,10 +71,10 @@
|
|||||||
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
||||||
<div
|
<div
|
||||||
v-if="terminalVisible && isError"
|
v-if="terminalVisible && isError"
|
||||||
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
|
class="absolute inset-x-4 bottom-4 z-10 mx-auto max-w-4xl"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
|
class="h-[300px] rounded-lg border border-neutral-700 bg-neutral-900/95 p-4"
|
||||||
>
|
>
|
||||||
<BaseTerminal @created="terminalCreated" />
|
<BaseTerminal @created="terminalCreated" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
|
|||||||
|
|
||||||
### Node.js & Playwright Prerequisites
|
### Node.js & Playwright Prerequisites
|
||||||
|
|
||||||
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
|
Ensure you have the Node.js version from `.nvmrc` installed (currently v24).
|
||||||
|
Then, set up the Chromium test driver:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm exec playwright install chromium --with-deps
|
pnpm exec playwright install chromium --with-deps
|
||||||
|
|||||||
@@ -36,14 +36,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"Node name for S&R": "CheckpointLoaderSimple",
|
"Node name for S&R": "CheckpointLoaderSimple",
|
||||||
"cnr_id": "comfy-core",
|
"cnr_id": "comfy-core",
|
||||||
"ver": "0.3.65",
|
"ver": "0.3.65"
|
||||||
"models": [
|
|
||||||
{
|
|
||||||
"name": "v1-5-pruned-emaonly-fp16.safetensors",
|
|
||||||
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
|
|
||||||
"directory": "checkpoints"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -206,9 +206,7 @@ export class ComfyPage {
|
|||||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||||
this.runButton = page
|
this.runButton = page.getByTestId(TestIds.topbar.queueButton)
|
||||||
.getByTestId(TestIds.topbar.queueButton)
|
|
||||||
.getByRole('button', { name: 'Run' })
|
|
||||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||||
|
|
||||||
this.searchBox = new ComfyNodeSearchBox(page)
|
this.searchBox = new ComfyNodeSearchBox(page)
|
||||||
@@ -432,7 +430,10 @@ export const comfyPageFixture = base.extend<{
|
|||||||
'Comfy.VueNodes.AutoScaleLayout': false,
|
'Comfy.VueNodes.AutoScaleLayout': false,
|
||||||
// Disable toast warning about version compatibility, as they may or
|
// Disable toast warning about version compatibility, as they may or
|
||||||
// may not appear - depending on upstream ComfyUI dependencies
|
// may not appear - depending on upstream ComfyUI dependencies
|
||||||
'Comfy.VersionCompatibility.DisableWarnings': true
|
'Comfy.VersionCompatibility.DisableWarnings': true,
|
||||||
|
// Browser tests should opt into missing-model warnings explicitly so
|
||||||
|
// workflows do not render differently based on models present on disk.
|
||||||
|
'Comfy.Workflow.ShowMissingModelsWarning': false
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -172,6 +172,19 @@ export class VueNodeHelpers {
|
|||||||
async enterSubgraph(nodeId?: string): Promise<void> {
|
async enterSubgraph(nodeId?: string): Promise<void> {
|
||||||
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
|
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
|
||||||
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
|
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
|
||||||
await editButton.click()
|
|
||||||
|
// The footer tab button extends below the node body (visible area),
|
||||||
|
// but its bounding box center overlaps the node body div.
|
||||||
|
// Click at the bottom 25% of the button which is the genuinely visible
|
||||||
|
// and unobstructed area outside the node body boundary.
|
||||||
|
const box = await editButton.boundingBox()
|
||||||
|
if (!box) {
|
||||||
|
throw new Error(
|
||||||
|
'subgraph-enter-button has no bounding box: element may be hidden or not in DOM'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await editButton.click({
|
||||||
|
position: { x: box.width / 2, y: box.height * 0.75 }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const TestIds = {
|
|||||||
},
|
},
|
||||||
topbar: {
|
topbar: {
|
||||||
queueButton: 'queue-button',
|
queueButton: 'queue-button',
|
||||||
|
queueModeMenuTrigger: 'queue-mode-menu-trigger',
|
||||||
saveButton: 'save-workflow-button'
|
saveButton: 'save-workflow-button'
|
||||||
},
|
},
|
||||||
nodeLibrary: {
|
nodeLibrary: {
|
||||||
|
|||||||
@@ -29,8 +29,10 @@ class ComfyQueueButton {
|
|||||||
public readonly dropdownButton: Locator
|
public readonly dropdownButton: Locator
|
||||||
constructor(public readonly actionbar: ComfyActionbar) {
|
constructor(public readonly actionbar: ComfyActionbar) {
|
||||||
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
|
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
|
||||||
this.primaryButton = this.root.locator('.p-splitbutton-button')
|
this.primaryButton = this.root
|
||||||
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
|
this.dropdownButton = actionbar.root.getByTestId(
|
||||||
|
TestIds.topbar.queueModeMenuTrigger
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toggleOptions() {
|
public async toggleOptions() {
|
||||||
|
|||||||
@@ -89,6 +89,17 @@ test.describe('Execution error', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Missing models warning', () => {
|
test.describe('Missing models warning', () => {
|
||||||
|
test('Should be disabled by default in browser tests', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.workflow.loadWorkflow('missing/missing_models')
|
||||||
|
|
||||||
|
const dialogTitle = comfyPage.page.getByText(
|
||||||
|
'This workflow is missing models'
|
||||||
|
)
|
||||||
|
await expect(dialogTitle).not.toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
await comfyPage.settings.setSetting(
|
await comfyPage.settings.setSetting(
|
||||||
'Comfy.Workflow.ShowMissingModelsWarning',
|
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 100 KiB |
@@ -819,16 +819,13 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
|
|||||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||||
const activeWorkflowName =
|
const activeWorkflowName =
|
||||||
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
|
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
|
||||||
const workflowPathA = `${workflowA}.json`
|
|
||||||
const workflowPathB = `${workflowB}.json`
|
|
||||||
|
|
||||||
expect(openWorkflows).toEqual(
|
expect(openWorkflows).toEqual(
|
||||||
expect.arrayContaining([workflowPathA, workflowPathB])
|
expect.arrayContaining([workflowA, workflowB])
|
||||||
)
|
)
|
||||||
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
|
expect(openWorkflows.indexOf(workflowA)).toBeLessThan(
|
||||||
openWorkflows.indexOf(workflowPathB)
|
openWorkflows.indexOf(workflowB)
|
||||||
)
|
)
|
||||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
expect(activeWorkflowName).toEqual(workflowB)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -35,18 +35,21 @@ test.describe(
|
|||||||
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
|
||||||
comfyPage
|
comfyPage
|
||||||
}) => {
|
}) => {
|
||||||
const waitForUpload = filesWithUpload.has(fileName)
|
const shouldUpload = filesWithUpload.has(fileName)
|
||||||
await comfyPage.dragDrop.dragAndDropFile(
|
const uploadRequestPromise = shouldUpload
|
||||||
`workflowInMedia/${fileName}`,
|
? comfyPage.page.waitForRequest((req) =>
|
||||||
{ waitForUpload }
|
req.url().includes('/upload/')
|
||||||
)
|
)
|
||||||
if (waitForUpload) {
|
: null
|
||||||
await comfyPage.page.waitForResponse(
|
|
||||||
(resp) => resp.url().includes('/view') && resp.status() !== 0,
|
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
|
||||||
{ timeout: 10000 }
|
|
||||||
)
|
if (uploadRequestPromise) {
|
||||||
|
const request = await uploadRequestPromise
|
||||||
|
expect(request.url()).toContain('/upload/')
|
||||||
|
} else {
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
||||||
}
|
}
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -13,9 +13,9 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('loads from inserted workflow', async ({ comfyPage }) => {
|
test('loads from inserted workflow', async ({ comfyPage }) => {
|
||||||
const workflowName = 'single_connected_reroute_node.json'
|
const workflowName = 'single_connected_reroute_node'
|
||||||
await comfyPage.workflow.setupWorkflowsDirectory({
|
await comfyPage.workflow.setupWorkflowsDirectory({
|
||||||
[workflowName]: 'links/single_connected_reroute_node.json'
|
[`${workflowName}.json`]: `links/${workflowName}.json`
|
||||||
})
|
})
|
||||||
await comfyPage.setup()
|
await comfyPage.setup()
|
||||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
@@ -21,14 +21,12 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
test('Can create new blank workflow', async ({ comfyPage }) => {
|
test('Can create new blank workflow', async ({ comfyPage }) => {
|
||||||
const tab = comfyPage.menu.workflowsTab
|
const tab = comfyPage.menu.workflowsTab
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
|
||||||
'*Unsaved Workflow.json'
|
|
||||||
])
|
|
||||||
|
|
||||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||||
'*Unsaved Workflow.json',
|
'*Unsaved Workflow',
|
||||||
'*Unsaved Workflow (2).json'
|
'*Unsaved Workflow (2)'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -41,37 +39,37 @@ test.describe('Workflows sidebar', () => {
|
|||||||
const tab = comfyPage.menu.workflowsTab
|
const tab = comfyPage.menu.workflowsTab
|
||||||
await tab.open()
|
await tab.open()
|
||||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||||
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
|
expect.arrayContaining(['workflow1', 'workflow2'])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can duplicate workflow', async ({ comfyPage }) => {
|
test('Can duplicate workflow', async ({ comfyPage }) => {
|
||||||
const tab = comfyPage.menu.workflowsTab
|
const tab = comfyPage.menu.workflowsTab
|
||||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
await comfyPage.menu.topbar.saveWorkflow('workflow1')
|
||||||
|
|
||||||
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
|
||||||
expect.arrayContaining(['workflow1.json'])
|
expect.arrayContaining(['workflow1'])
|
||||||
)
|
)
|
||||||
|
|
||||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow1.json',
|
'workflow1',
|
||||||
'*workflow1 (Copy).json'
|
'*workflow1 (Copy)'
|
||||||
])
|
])
|
||||||
|
|
||||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow1.json',
|
'workflow1',
|
||||||
'*workflow1 (Copy).json',
|
'*workflow1 (Copy)',
|
||||||
'*workflow1 (Copy) (2).json'
|
'*workflow1 (Copy) (2)'
|
||||||
])
|
])
|
||||||
|
|
||||||
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow1.json',
|
'workflow1',
|
||||||
'*workflow1 (Copy).json',
|
'*workflow1 (Copy)',
|
||||||
'*workflow1 (Copy) (2).json',
|
'*workflow1 (Copy) (2)',
|
||||||
'*workflow1 (Copy) (3).json'
|
'*workflow1 (Copy) (3)'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,12 +83,12 @@ test.describe('Workflows sidebar', () => {
|
|||||||
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
|
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
|
||||||
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
|
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
|
||||||
|
|
||||||
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
|
await tab.insertWorkflow(tab.getPersistedItem('workflow1'))
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.nodeOps.getNodeCount())
|
.poll(() => comfyPage.nodeOps.getNodeCount())
|
||||||
.toEqual(originalNodeCount + 1)
|
.toEqual(originalNodeCount + 1)
|
||||||
|
|
||||||
await tab.getPersistedItem('workflow1.json').click()
|
await tab.getPersistedItem('workflow1').click()
|
||||||
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
|
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,22 +111,22 @@ test.describe('Workflows sidebar', () => {
|
|||||||
const openedWorkflow = tab.getOpenedItem('foo/bar')
|
const openedWorkflow = tab.getOpenedItem('foo/bar')
|
||||||
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
|
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||||
'*Unsaved Workflow.json',
|
'*Unsaved Workflow',
|
||||||
'foo/baz.json'
|
'foo/baz'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can save workflow as', async ({ comfyPage }) => {
|
test('Can save workflow as', async ({ comfyPage }) => {
|
||||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
|
await comfyPage.menu.topbar.saveWorkflowAs('workflow3')
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
|
.toEqual(['*Unsaved Workflow', 'workflow3'])
|
||||||
|
|
||||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
|
await comfyPage.menu.topbar.saveWorkflowAs('workflow4')
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||||
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
|
.toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Exported workflow does not contain localized slot names', async ({
|
test('Exported workflow does not contain localized slot names', async ({
|
||||||
@@ -184,15 +182,15 @@ test.describe('Workflows sidebar', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
await comfyPage.menu.topbar.saveWorkflow('workflow5')
|
||||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow5.json'
|
'workflow5'
|
||||||
])
|
])
|
||||||
|
|
||||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
await comfyPage.menu.topbar.saveWorkflowAs('workflow5')
|
||||||
await comfyPage.confirmDialog.click('overwrite')
|
await comfyPage.confirmDialog.click('overwrite')
|
||||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow5.json'
|
'workflow5'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -212,25 +210,25 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
|
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
|
||||||
const topbar = comfyPage.menu.topbar
|
const topbar = comfyPage.menu.topbar
|
||||||
await topbar.saveWorkflow('workflow1.json')
|
await topbar.saveWorkflow('workflow1')
|
||||||
await topbar.saveWorkflowAs('workflow2.json')
|
await topbar.saveWorkflowAs('workflow2')
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||||
.toEqual(['workflow1.json', 'workflow2.json'])
|
.toEqual(['workflow1', 'workflow2'])
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||||
.toEqual('workflow2.json')
|
.toEqual('workflow2')
|
||||||
|
|
||||||
await topbar.saveWorkflowAs('workflow1.json')
|
await topbar.saveWorkflowAs('workflow1')
|
||||||
await comfyPage.confirmDialog.click('overwrite')
|
await comfyPage.confirmDialog.click('overwrite')
|
||||||
// The old workflow1.json should be deleted and the new one should be saved.
|
// The old workflow1 should be deleted and the new one should be saved.
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
|
||||||
.toEqual(['workflow2.json', 'workflow1.json'])
|
.toEqual(['workflow2', 'workflow1'])
|
||||||
await expect
|
await expect
|
||||||
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
|
||||||
.toEqual('workflow1.json')
|
.toEqual('workflow1')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Does not report warning when switching between opened workflows', async ({
|
test('Does not report warning when switching between opened workflows', async ({
|
||||||
@@ -266,17 +264,15 @@ test.describe('Workflows sidebar', () => {
|
|||||||
)
|
)
|
||||||
await closeButton.click()
|
await closeButton.click()
|
||||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'*Unsaved Workflow.json'
|
'*Unsaved Workflow'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can close saved workflow with command', async ({ comfyPage }) => {
|
test('Can close saved workflow with command', async ({ comfyPage }) => {
|
||||||
const tab = comfyPage.menu.workflowsTab
|
const tab = comfyPage.menu.workflowsTab
|
||||||
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
|
await comfyPage.menu.topbar.saveWorkflow('workflow1')
|
||||||
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
|
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
|
||||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
|
||||||
'*Unsaved Workflow.json'
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
||||||
@@ -284,7 +280,7 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
const { topbar, workflowsTab } = comfyPage.menu
|
const { topbar, workflowsTab } = comfyPage.menu
|
||||||
|
|
||||||
const filename = 'workflow18.json'
|
const filename = 'workflow18'
|
||||||
await topbar.saveWorkflow(filename)
|
await topbar.saveWorkflow(filename)
|
||||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||||
|
|
||||||
@@ -295,14 +291,14 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'*Unsaved Workflow.json'
|
'*Unsaved Workflow'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can delete workflows', async ({ comfyPage }) => {
|
test('Can delete workflows', async ({ comfyPage }) => {
|
||||||
const { topbar, workflowsTab } = comfyPage.menu
|
const { topbar, workflowsTab } = comfyPage.menu
|
||||||
|
|
||||||
const filename = 'workflow18.json'
|
const filename = 'workflow18'
|
||||||
await topbar.saveWorkflow(filename)
|
await topbar.saveWorkflow(filename)
|
||||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||||
|
|
||||||
@@ -314,7 +310,7 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'*Unsaved Workflow.json'
|
'*Unsaved Workflow'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -326,13 +322,11 @@ test.describe('Workflows sidebar', () => {
|
|||||||
const { workflowsTab } = comfyPage.menu
|
const { workflowsTab } = comfyPage.menu
|
||||||
await workflowsTab.open()
|
await workflowsTab.open()
|
||||||
|
|
||||||
await workflowsTab
|
await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' })
|
||||||
.getPersistedItem('workflow1.json')
|
|
||||||
.click({ button: 'right' })
|
|
||||||
await comfyPage.contextMenu.clickMenuItem('Duplicate')
|
await comfyPage.contextMenu.clickMenuItem('Duplicate')
|
||||||
await expect
|
await expect
|
||||||
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
.poll(() => workflowsTab.getOpenedWorkflowNames())
|
||||||
.toEqual(['*Unsaved Workflow.json', '*workflow1 (Copy).json'])
|
.toEqual(['*Unsaved Workflow', '*workflow1 (Copy)'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
|
||||||
@@ -344,7 +338,7 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
// Wait for workflow to appear in Browse section after sync
|
// Wait for workflow to appear in Browse section after sync
|
||||||
const workflowItem =
|
const workflowItem =
|
||||||
comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json')
|
comfyPage.menu.workflowsTab.getPersistedItem('workflow1')
|
||||||
await expect(workflowItem).toBeVisible({ timeout: 3000 })
|
await expect(workflowItem).toBeVisible({ timeout: 3000 })
|
||||||
|
|
||||||
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
|
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||||
@@ -361,7 +355,7 @@ test.describe('Workflows sidebar', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await comfyPage.page.dragAndDrop(
|
await comfyPage.page.dragAndDrop(
|
||||||
'.comfyui-workflows-browse .node-label:has-text("workflow1.json")',
|
'.comfyui-workflows-browse .node-label:has-text("workflow1")',
|
||||||
'#graph-canvas',
|
'#graph-canvas',
|
||||||
{ targetPosition }
|
{ targetPosition }
|
||||||
)
|
)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
@@ -22,8 +22,10 @@ test.describe('Vue Node Bypass', () => {
|
|||||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||||
|
|
||||||
const checkpointNode =
|
const checkpointNode = comfyPage.page
|
||||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
.locator('[data-node-id]')
|
||||||
|
.filter({ hasText: 'Load Checkpoint' })
|
||||||
|
.getByTestId('node-inner-wrapper')
|
||||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
@@ -41,8 +43,14 @@ test.describe('Vue Node Bypass', () => {
|
|||||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||||
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
|
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
|
||||||
|
|
||||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
const checkpointNode = comfyPage.page
|
||||||
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
|
.locator('[data-node-id]')
|
||||||
|
.filter({ hasText: 'Load Checkpoint' })
|
||||||
|
.getByTestId('node-inner-wrapper')
|
||||||
|
const ksamplerNode = comfyPage.page
|
||||||
|
.locator('[data-node-id]')
|
||||||
|
.filter({ hasText: 'KSampler' })
|
||||||
|
.getByTestId('node-inner-wrapper')
|
||||||
|
|
||||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
@@ -3,7 +3,7 @@ import {
|
|||||||
comfyPageFixture as test
|
comfyPageFixture as test
|
||||||
} from '../../../fixtures/ComfyPage'
|
} from '../../../fixtures/ComfyPage'
|
||||||
|
|
||||||
const ERROR_CLASS = /border-node-stroke-error/
|
const ERROR_CLASS = /ring-destructive-background/
|
||||||
|
|
||||||
test.describe('Vue Node Error', () => {
|
test.describe('Vue Node Error', () => {
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
@@ -18,9 +18,10 @@ test.describe('Vue Node Error', () => {
|
|||||||
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
|
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
|
||||||
|
|
||||||
// Expect error state on missing unknown node
|
// Expect error state on missing unknown node
|
||||||
const unknownNode = comfyPage.page.locator('[data-node-id]').filter({
|
const unknownNode = comfyPage.page
|
||||||
hasText: 'UNKNOWN NODE'
|
.locator('[data-node-id]')
|
||||||
})
|
.filter({ hasText: 'UNKNOWN NODE' })
|
||||||
|
.getByTestId('node-inner-wrapper')
|
||||||
await expect(unknownNode).toHaveClass(ERROR_CLASS)
|
await expect(unknownNode).toHaveClass(ERROR_CLASS)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -31,7 +32,10 @@ test.describe('Vue Node Error', () => {
|
|||||||
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
|
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
|
||||||
await comfyPage.runButton.click()
|
await comfyPage.runButton.click()
|
||||||
|
|
||||||
const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error')
|
const raiseErrorNode = comfyPage.page
|
||||||
|
.locator('[data-node-id]')
|
||||||
|
.filter({ hasText: 'Raise Error' })
|
||||||
|
.getByTestId('node-inner-wrapper')
|
||||||
await expect(raiseErrorNode).toHaveClass(ERROR_CLASS)
|
await expect(raiseErrorNode).toHaveClass(ERROR_CLASS)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@comfyorg/comfyui-frontend",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"version": "1.41.13",
|
"version": "1.42.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Official front-end implementation of ComfyUI",
|
"description": "Official front-end implementation of ComfyUI",
|
||||||
"homepage": "https://comfy.org",
|
"homepage": "https://comfy.org",
|
||||||
@@ -195,6 +195,9 @@
|
|||||||
"zip-dir": "^2.0.0",
|
"zip-dir": "^2.0.0",
|
||||||
"zod-to-json-schema": "catalog:"
|
"zod-to-json-schema": "catalog:"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "24.x"
|
||||||
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "catalog:"
|
"vite": "catalog:"
|
||||||
|
|||||||
@@ -166,13 +166,22 @@ describe('TopMenuSection', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authentication state', () => {
|
describe('authentication state', () => {
|
||||||
|
function createLegacyTabBarWrapper() {
|
||||||
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
||||||
|
const settingStore = useSettingStore(pinia)
|
||||||
|
vi.mocked(settingStore.get).mockImplementation((key) =>
|
||||||
|
key === 'Comfy.UI.TabBarLayout' ? 'Legacy' : undefined
|
||||||
|
)
|
||||||
|
return createWrapper({ pinia })
|
||||||
|
}
|
||||||
|
|
||||||
describe('when user is logged in', () => {
|
describe('when user is logged in', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockData.isLoggedIn = true
|
mockData.isLoggedIn = true
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should display CurrentUserButton and not display LoginButton', () => {
|
it('should display CurrentUserButton and not display LoginButton', () => {
|
||||||
const wrapper = createWrapper()
|
const wrapper = createLegacyTabBarWrapper()
|
||||||
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(true)
|
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(true)
|
||||||
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
|
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -186,7 +195,7 @@ describe('TopMenuSection', () => {
|
|||||||
describe('on desktop platform', () => {
|
describe('on desktop platform', () => {
|
||||||
it('should display LoginButton and not display CurrentUserButton', () => {
|
it('should display LoginButton and not display CurrentUserButton', () => {
|
||||||
mockData.isDesktop = true
|
mockData.isDesktop = true
|
||||||
const wrapper = createWrapper()
|
const wrapper = createLegacyTabBarWrapper()
|
||||||
expect(wrapper.findComponent(LoginButton).exists()).toBe(true)
|
expect(wrapper.findComponent(LoginButton).exists()).toBe(true)
|
||||||
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
|
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -194,7 +203,7 @@ describe('TopMenuSection', () => {
|
|||||||
|
|
||||||
describe('on web platform', () => {
|
describe('on web platform', () => {
|
||||||
it('should not display CurrentUserButton and not display LoginButton', () => {
|
it('should not display CurrentUserButton and not display LoginButton', () => {
|
||||||
const wrapper = createWrapper()
|
const wrapper = createLegacyTabBarWrapper()
|
||||||
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
|
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
|
||||||
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
|
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ const isActionbarFloating = computed(
|
|||||||
() => isActionbarEnabled.value && !isActionbarDocked.value
|
() => isActionbarEnabled.value && !isActionbarDocked.value
|
||||||
)
|
)
|
||||||
const isIntegratedTabBar = computed(
|
const isIntegratedTabBar = computed(
|
||||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
() => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy'
|
||||||
)
|
)
|
||||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||||
useQueueFeatureFlags()
|
useQueueFeatureFlags()
|
||||||
|
|||||||
98
src/components/actionbar/BatchCountEdit.test.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||||
|
|
||||||
|
import BatchCountEdit from './BatchCountEdit.vue'
|
||||||
|
|
||||||
|
const maxBatchCount = 16
|
||||||
|
|
||||||
|
vi.mock('@/platform/settings/settingStore', () => ({
|
||||||
|
useSettingStore: () => ({
|
||||||
|
get: (settingId: string) =>
|
||||||
|
settingId === 'Comfy.QueueButton.BatchCountLimit' ? maxBatchCount : 1
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
g: {
|
||||||
|
increment: 'Increment',
|
||||||
|
decrement: 'Decrement'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
batchCount: 'Batch Count'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createWrapper(initialBatchCount = 1) {
|
||||||
|
const pinia = createTestingPinia({
|
||||||
|
createSpy: vi.fn,
|
||||||
|
stubActions: false,
|
||||||
|
initialState: {
|
||||||
|
queueSettingsStore: {
|
||||||
|
batchCount: initialBatchCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mount(BatchCountEdit, {
|
||||||
|
global: {
|
||||||
|
plugins: [pinia, i18n],
|
||||||
|
directives: {
|
||||||
|
tooltip: () => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
|
|
||||||
|
return { wrapper, queueSettingsStore }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BatchCountEdit', () => {
|
||||||
|
it('doubles the current batch count when increment is clicked', async () => {
|
||||||
|
const { wrapper, queueSettingsStore } = createWrapper(3)
|
||||||
|
|
||||||
|
await wrapper.get('button[aria-label="Increment"]').trigger('click')
|
||||||
|
|
||||||
|
expect(queueSettingsStore.batchCount).toBe(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('halves the current batch count when decrement is clicked', async () => {
|
||||||
|
const { wrapper, queueSettingsStore } = createWrapper(9)
|
||||||
|
|
||||||
|
await wrapper.get('button[aria-label="Decrement"]').trigger('click')
|
||||||
|
|
||||||
|
expect(queueSettingsStore.batchCount).toBe(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps typed values to queue limits on blur', async () => {
|
||||||
|
const { wrapper, queueSettingsStore } = createWrapper(2)
|
||||||
|
const input = wrapper.get('input')
|
||||||
|
|
||||||
|
await input.setValue('999')
|
||||||
|
await input.trigger('blur')
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(queueSettingsStore.batchCount).toBe(maxBatchCount)
|
||||||
|
expect((input.element as HTMLInputElement).value).toBe(
|
||||||
|
String(maxBatchCount)
|
||||||
|
)
|
||||||
|
|
||||||
|
await input.setValue('0')
|
||||||
|
await input.trigger('blur')
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(queueSettingsStore.batchCount).toBe(1)
|
||||||
|
expect((input.element as HTMLInputElement).value).toBe('1')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,71 +1,129 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-tooltip.bottom="{
|
v-tooltip.bottom="{
|
||||||
value: $t('menu.batchCount'),
|
value: t('menu.batchCount'),
|
||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
class="batch-count"
|
class="batch-count h-full"
|
||||||
:aria-label="$t('menu.batchCount')"
|
:aria-label="t('menu.batchCount')"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<div
|
||||||
v-model="batchCount"
|
class="flex h-full w-14 overflow-hidden rounded-l-lg bg-secondary-background"
|
||||||
class="w-14"
|
>
|
||||||
:min="minQueueCount"
|
<input
|
||||||
:max="maxQueueCount"
|
ref="batchCountInputRef"
|
||||||
fluid
|
v-model="batchCountInput"
|
||||||
show-buttons
|
type="text"
|
||||||
:pt="{
|
inputmode="numeric"
|
||||||
incrementButton: {
|
:aria-label="t('menu.batchCount')"
|
||||||
class: 'w-6',
|
:class="inputClass"
|
||||||
onmousedown: () => {
|
@focus="onInputFocus"
|
||||||
handleClick(true)
|
@input="onInput"
|
||||||
}
|
@blur="onInputBlur"
|
||||||
},
|
@keydown.enter.prevent="onInputEnter"
|
||||||
decrementButton: {
|
/>
|
||||||
class: 'w-6',
|
<div class="flex h-full w-6 flex-col">
|
||||||
onmousedown: () => {
|
<Button
|
||||||
handleClick(false)
|
variant="secondary"
|
||||||
}
|
size="unset"
|
||||||
}
|
:aria-label="t('g.increment')"
|
||||||
}"
|
:class="cn(stepButtonClass, incrementButtonClass)"
|
||||||
/>
|
:disabled="isIncrementDisabled"
|
||||||
|
@click="incrementBatchCount"
|
||||||
|
>
|
||||||
|
<TinyChevronIcon rotate-up />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('g.decrement')"
|
||||||
|
:class="cn(stepButtonClass, decrementButtonClass)"
|
||||||
|
:disabled="isDecrementDisabled"
|
||||||
|
@click="decrementBatchCount"
|
||||||
|
>
|
||||||
|
<TinyChevronIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import InputNumber from 'primevue/inputnumber'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
import TinyChevronIcon from './TinyChevronIcon.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const queueSettingsStore = useQueueSettingsStore()
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
const { batchCount } = storeToRefs(queueSettingsStore)
|
const { batchCount } = storeToRefs(queueSettingsStore)
|
||||||
const minQueueCount = 1
|
|
||||||
|
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const minQueueCount = 1
|
||||||
const maxQueueCount = computed(() =>
|
const maxQueueCount = computed(() =>
|
||||||
settingStore.get('Comfy.QueueButton.BatchCountLimit')
|
settingStore.get('Comfy.QueueButton.BatchCountLimit')
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = (increment: boolean) => {
|
const batchCountInputRef = ref<HTMLInputElement | null>(null)
|
||||||
let newCount: number
|
const batchCountInput = ref(String(batchCount.value))
|
||||||
if (increment) {
|
const isEditing = ref(false)
|
||||||
const originalCount = batchCount.value - 1
|
|
||||||
newCount = Math.min(originalCount * 2, maxQueueCount.value)
|
|
||||||
} else {
|
|
||||||
const originalCount = batchCount.value + 1
|
|
||||||
newCount = Math.floor(originalCount / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
batchCount.value = newCount
|
const isIncrementDisabled = computed(
|
||||||
|
() => batchCount.value >= maxQueueCount.value
|
||||||
|
)
|
||||||
|
const isDecrementDisabled = computed(() => batchCount.value <= minQueueCount)
|
||||||
|
const inputClass =
|
||||||
|
'h-full min-w-0 flex-1 border-none bg-secondary-background pl-1 pr-0 text-center text-sm font-normal tabular-nums text-base-foreground outline-none'
|
||||||
|
const stepButtonClass =
|
||||||
|
'h-1/2 w-full rounded-none border-none p-0 text-muted-foreground hover:bg-secondary-background-hover disabled:cursor-not-allowed disabled:opacity-50'
|
||||||
|
const incrementButtonClass = 'rounded-tr-none border-b border-border-subtle'
|
||||||
|
const decrementButtonClass = 'rounded-br-none'
|
||||||
|
|
||||||
|
watch(batchCount, (nextBatchCount) => {
|
||||||
|
if (!isEditing.value) {
|
||||||
|
batchCountInput.value = String(nextBatchCount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const clampBatchCount = (nextBatchCount: number): number =>
|
||||||
|
Math.min(Math.max(nextBatchCount, minQueueCount), maxQueueCount.value)
|
||||||
|
|
||||||
|
const setBatchCount = (nextBatchCount: number) => {
|
||||||
|
batchCount.value = clampBatchCount(nextBatchCount)
|
||||||
|
batchCountInput.value = String(batchCount.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementBatchCount = () => {
|
||||||
|
setBatchCount(batchCount.value * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrementBatchCount = () => {
|
||||||
|
setBatchCount(Math.floor(batchCount.value / 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInputFocus = () => {
|
||||||
|
isEditing.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
batchCountInput.value = input.value.replace(/[^0-9]/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInputBlur = () => {
|
||||||
|
isEditing.value = false
|
||||||
|
const parsedInput = Number.parseInt(batchCountInput.value, 10)
|
||||||
|
setBatchCount(Number.isNaN(parsedInput) ? minQueueCount : parsedInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInputEnter = () => {
|
||||||
|
batchCountInputRef.value?.blur()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.p-inputtext) {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { defineComponent, nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -41,28 +41,9 @@ vi.mock('@/stores/workspaceStore', () => ({
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const SplitButtonStub = defineComponent({
|
const BatchCountEditStub = {
|
||||||
name: 'SplitButton',
|
template: '<div data-testid="batch-count-edit" />'
|
||||||
props: {
|
}
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
severity: {
|
|
||||||
type: String,
|
|
||||||
default: 'primary'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<button
|
|
||||||
data-testid="split-button"
|
|
||||||
:data-label="label"
|
|
||||||
:data-severity="severity"
|
|
||||||
>
|
|
||||||
<slot name="icon" />
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
})
|
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
legacy: false,
|
legacy: false,
|
||||||
@@ -107,14 +88,26 @@ function createWrapper() {
|
|||||||
tooltip: () => {}
|
tooltip: () => {}
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
SplitButton: SplitButtonStub,
|
BatchCountEdit: BatchCountEditStub,
|
||||||
BatchCountEdit: true
|
DropdownMenuRoot: { template: '<div><slot /></div>' },
|
||||||
|
DropdownMenuTrigger: { template: '<div><slot /></div>' },
|
||||||
|
DropdownMenuPortal: { template: '<div><slot /></div>' },
|
||||||
|
DropdownMenuContent: { template: '<div><slot /></div>' },
|
||||||
|
DropdownMenuItem: { template: '<div><slot /></div>' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ComfyQueueButton', () => {
|
describe('ComfyQueueButton', () => {
|
||||||
|
it('renders the batch count control before the run button', () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const controls = wrapper.get('.queue-button-group').element.children
|
||||||
|
|
||||||
|
expect(controls[0]?.getAttribute('data-testid')).toBe('batch-count-edit')
|
||||||
|
expect(controls[1]?.getAttribute('data-testid')).toBe('queue-button')
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps the run instant presentation while idle even with active jobs', async () => {
|
it('keeps the run instant presentation while idle even with active jobs', async () => {
|
||||||
const wrapper = createWrapper()
|
const wrapper = createWrapper()
|
||||||
const queueSettingsStore = useQueueSettingsStore()
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
@@ -124,10 +117,10 @@ describe('ComfyQueueButton', () => {
|
|||||||
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
|
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||||
|
|
||||||
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
expect(queueButton.text()).toContain('Run (Instant)')
|
||||||
expect(splitButton.attributes('data-severity')).toBe('primary')
|
expect(queueButton.attributes('data-variant')).toBe('primary')
|
||||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -138,10 +131,10 @@ describe('ComfyQueueButton', () => {
|
|||||||
queueSettingsStore.mode = 'instant-running'
|
queueSettingsStore.mode = 'instant-running'
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||||
|
|
||||||
expect(splitButton.attributes('data-label')).toBe('Stop Run (Instant)')
|
expect(queueButton.text()).toContain('Stop Run (Instant)')
|
||||||
expect(splitButton.attributes('data-severity')).toBe('danger')
|
expect(queueButton.attributes('data-variant')).toBe('destructive')
|
||||||
expect(wrapper.find('.icon-\\[lucide--square\\]').exists()).toBe(true)
|
expect(wrapper.find('.icon-\\[lucide--square\\]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,19 +152,17 @@ describe('ComfyQueueButton', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
expect(queueSettingsStore.mode).toBe('instant-idle')
|
expect(queueSettingsStore.mode).toBe('instant-idle')
|
||||||
const splitButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
|
const queueButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
|
||||||
expect(splitButtonWhileStopping.attributes('data-label')).toBe(
|
expect(queueButtonWhileStopping.text()).toContain('Run (Instant)')
|
||||||
'Run (Instant)'
|
expect(queueButtonWhileStopping.attributes('data-variant')).toBe('primary')
|
||||||
)
|
|
||||||
expect(splitButtonWhileStopping.attributes('data-severity')).toBe('primary')
|
|
||||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||||
|
|
||||||
expect(commandStore.execute).not.toHaveBeenCalled()
|
expect(commandStore.execute).not.toHaveBeenCalled()
|
||||||
|
|
||||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||||
expect(queueSettingsStore.mode).toBe('instant-idle')
|
expect(queueSettingsStore.mode).toBe('instant-idle')
|
||||||
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
expect(queueButton.text()).toContain('Run (Instant)')
|
||||||
expect(splitButton.attributes('data-severity')).toBe('primary')
|
expect(queueButton.attributes('data-variant')).toBe('primary')
|
||||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,83 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="queue-button-group flex">
|
<ButtonGroup
|
||||||
<SplitButton
|
class="queue-button-group h-8 rounded-lg bg-secondary-background"
|
||||||
|
>
|
||||||
|
<BatchCountEdit />
|
||||||
|
<Button
|
||||||
v-tooltip.bottom="{
|
v-tooltip.bottom="{
|
||||||
value: queueButtonTooltip,
|
value: queueButtonTooltip,
|
||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
class="comfyui-queue-button"
|
:variant="queueButtonVariant"
|
||||||
:label="queueButtonLabel"
|
size="unset"
|
||||||
:severity="queueButtonSeverity"
|
:class="queueActionButtonClass"
|
||||||
size="small"
|
|
||||||
:model="queueModeMenuItems"
|
|
||||||
data-testid="queue-button"
|
data-testid="queue-button"
|
||||||
|
:data-variant="queueButtonVariant"
|
||||||
@click="queuePrompt"
|
@click="queuePrompt"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<i :class="cn(iconClass, 'size-4')" />
|
||||||
<i :class="iconClass" />
|
{{ queueButtonLabel }}
|
||||||
</template>
|
</Button>
|
||||||
<template #item="{ item }">
|
|
||||||
|
<DropdownMenuRoot>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip="{
|
variant="secondary"
|
||||||
value: item.tooltip,
|
size="unset"
|
||||||
showDelay: 600
|
:class="queueMenuTriggerClass"
|
||||||
}"
|
:aria-label="t('menu.run')"
|
||||||
:variant="item.key === selectedQueueMode ? 'primary' : 'secondary'"
|
data-testid="queue-mode-menu-trigger"
|
||||||
size="sm"
|
|
||||||
class="w-full justify-start"
|
|
||||||
>
|
>
|
||||||
<i v-if="item.icon" :class="item.icon" />
|
<TinyChevronIcon />
|
||||||
{{ String(item.label ?? '') }}
|
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</DropdownMenuTrigger>
|
||||||
</SplitButton>
|
<DropdownMenuPortal>
|
||||||
<BatchCountEdit />
|
<DropdownMenuContent
|
||||||
</div>
|
:side-offset="4"
|
||||||
|
class="z-1000 min-w-44 rounded-lg border border-border-subtle bg-base-background p-1 shadow-interface"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="item in queueModeMenuItems"
|
||||||
|
:key="item.key"
|
||||||
|
as-child
|
||||||
|
@select.prevent="item.command"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-tooltip="{
|
||||||
|
value: item.tooltip,
|
||||||
|
showDelay: 600
|
||||||
|
}"
|
||||||
|
:variant="
|
||||||
|
item.key === selectedQueueMode ? 'primary' : 'secondary'
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
:class="queueMenuItemButtonClass"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
</ButtonGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRoot,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from 'reka-ui'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import type { MenuItem } from 'primevue/menuitem'
|
|
||||||
import SplitButton from 'primevue/splitbutton'
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import BatchCountEdit from '@/components/actionbar/BatchCountEdit.vue'
|
||||||
|
import TinyChevronIcon from '@/components/actionbar/TinyChevronIcon.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import ButtonGroup from '@/components/ui/button-group/ButtonGroup.vue'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
@@ -54,10 +89,9 @@ import {
|
|||||||
useQueueSettingsStore
|
useQueueSettingsStore
|
||||||
} from '@/stores/queueStore'
|
} from '@/stores/queueStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
||||||
|
|
||||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
|
||||||
|
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||||
|
|
||||||
@@ -69,50 +103,60 @@ const hasMissingNodes = computed(() =>
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
type QueueModeMenuKey = 'disabled' | 'change' | 'instant-idle'
|
type QueueModeMenuKey = 'disabled' | 'change' | 'instant-idle'
|
||||||
|
|
||||||
|
interface QueueModeMenuItem {
|
||||||
|
key: QueueModeMenuKey
|
||||||
|
label: string
|
||||||
|
tooltip: string
|
||||||
|
command: () => void
|
||||||
|
}
|
||||||
|
|
||||||
const selectedQueueMode = computed<QueueModeMenuKey>(() =>
|
const selectedQueueMode = computed<QueueModeMenuKey>(() =>
|
||||||
isInstantMode(queueMode.value) ? 'instant-idle' : queueMode.value
|
isInstantMode(queueMode.value) ? 'instant-idle' : queueMode.value
|
||||||
)
|
)
|
||||||
|
|
||||||
const queueModeMenuItemLookup = computed(() => {
|
const queueModeMenuItemLookup = computed<Record<string, QueueModeMenuItem>>(
|
||||||
const items: Record<string, MenuItem> = {
|
() => {
|
||||||
disabled: {
|
const items: Record<string, QueueModeMenuItem> = {
|
||||||
key: 'disabled',
|
disabled: {
|
||||||
label: t('menu.run'),
|
key: 'disabled',
|
||||||
tooltip: t('menu.disabledTooltip'),
|
label: t('menu.run'),
|
||||||
command: () => {
|
tooltip: t('menu.disabledTooltip'),
|
||||||
queueMode.value = 'disabled'
|
command: () => {
|
||||||
}
|
queueMode.value = 'disabled'
|
||||||
},
|
}
|
||||||
change: {
|
},
|
||||||
key: 'change',
|
change: {
|
||||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
key: 'change',
|
||||||
tooltip: t('menu.onChangeTooltip'),
|
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||||
command: () => {
|
tooltip: t('menu.onChangeTooltip'),
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
command: () => {
|
||||||
button_id: 'queue_mode_option_run_on_change_selected'
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
})
|
button_id: 'queue_mode_option_run_on_change_selected'
|
||||||
queueMode.value = 'change'
|
})
|
||||||
|
queueMode.value = 'change'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
items['instant-idle'] = {
|
items['instant-idle'] = {
|
||||||
key: 'instant-idle',
|
key: 'instant-idle',
|
||||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||||
tooltip: t('menu.instantTooltip'),
|
tooltip: t('menu.instantTooltip'),
|
||||||
command: () => {
|
command: () => {
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
button_id: 'queue_mode_option_run_instant_selected'
|
button_id: 'queue_mode_option_run_instant_selected'
|
||||||
})
|
})
|
||||||
queueMode.value = 'instant-idle'
|
queueMode.value = 'instant-idle'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
return items
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const activeQueueModeMenuItem = computed(() => {
|
const activeQueueModeMenuItem = computed(() => {
|
||||||
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
|
||||||
return (
|
return (
|
||||||
queueModeMenuItemLookup.value[selectedQueueMode.value] ||
|
queueModeMenuItemLookup.value[selectedQueueMode.value] ||
|
||||||
queueModeMenuItemLookup.value.disabled
|
queueModeMenuItemLookup.value.disabled
|
||||||
@@ -132,9 +176,13 @@ const queueButtonLabel = computed(() =>
|
|||||||
: String(activeQueueModeMenuItem.value?.label ?? '')
|
: String(activeQueueModeMenuItem.value?.label ?? '')
|
||||||
)
|
)
|
||||||
|
|
||||||
const queueButtonSeverity = computed(() =>
|
const queueButtonVariant = computed<'destructive' | 'primary'>(() =>
|
||||||
isStopInstantAction.value ? 'danger' : 'primary'
|
isStopInstantAction.value ? 'destructive' : 'primary'
|
||||||
)
|
)
|
||||||
|
const queueActionButtonClass = 'h-full rounded-lg gap-1.5 px-4 font-light'
|
||||||
|
const queueMenuTriggerClass =
|
||||||
|
'h-full w-6 rounded-l-none rounded-r-lg border-l border-border-subtle p-0 text-muted-foreground data-[state=open]:bg-secondary-background-hover'
|
||||||
|
const queueMenuItemButtonClass = 'w-full justify-start font-normal'
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const iconClass = computed(() => {
|
||||||
if (isStopInstantAction.value) {
|
if (isStopInstantAction.value) {
|
||||||
@@ -201,10 +249,3 @@ const queuePrompt = async (e: Event) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
26
src/components/actionbar/TinyChevronIcon.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
class="h-[5px] min-h-[5px] w-[8px] min-w-[8px]"
|
||||||
|
:class="{ 'rotate-180': rotateUp }"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="8"
|
||||||
|
height="5"
|
||||||
|
viewBox="0 0 8 5"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.650391 0.649902L3.65039 3.6499L6.65039 0.649902"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { rotateUp = false } = defineProps<{
|
||||||
|
rotateUp?: boolean
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -3,9 +3,15 @@ import { computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
||||||
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useAppMode } from '@/composables/useAppMode'
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
import {
|
||||||
|
openShareDialog,
|
||||||
|
prefetchShareDialog
|
||||||
|
} from '@/platform/workflow/sharing/composables/lazyShareDialog'
|
||||||
import { useAppModeStore } from '@/stores/appModeStore'
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
@@ -18,6 +24,8 @@ const workspaceStore = useWorkspaceStore()
|
|||||||
const { enableAppBuilder } = useAppMode()
|
const { enableAppBuilder } = useAppMode()
|
||||||
const appModeStore = useAppModeStore()
|
const appModeStore = useAppModeStore()
|
||||||
const { enterBuilder } = appModeStore
|
const { enterBuilder } = appModeStore
|
||||||
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
const { hasNodes } = storeToRefs(appModeStore)
|
const { hasNodes } = storeToRefs(appModeStore)
|
||||||
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
||||||
|
|
||||||
@@ -35,97 +43,77 @@ function openAssets() {
|
|||||||
function showApps() {
|
function showApps() {
|
||||||
void commandStore.execute('Workspace.ToggleSidebarTab.apps')
|
void commandStore.execute('Workspace.ToggleSidebarTab.apps')
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTemplates() {
|
|
||||||
useWorkflowTemplateSelectorDialog().show('sidebar')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pointer-events-auto flex flex-col gap-2">
|
<div class="pointer-events-auto flex flex-row items-start gap-2">
|
||||||
<WorkflowActionsDropdown source="app_mode_toolbar">
|
<div class="pointer-events-auto flex flex-col gap-2">
|
||||||
<template #button="{ hasUnseenItems }">
|
<Button
|
||||||
|
v-if="enableAppBuilder"
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('linearMode.appModeToolbar.appBuilder'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:disabled="!hasNodes"
|
||||||
|
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
|
||||||
|
class="size-10 rounded-lg"
|
||||||
|
@click="enterBuilder"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--hammer] size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="isCloud && flags.workflowSharingEnabled"
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('actionbar.shareTooltip'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('actionbar.shareTooltip')"
|
||||||
|
class="size-10 rounded-lg"
|
||||||
|
@click="() => openShareDialog().catch(toastErrorHandler)"
|
||||||
|
@pointerenter="prefetchShareDialog"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--send] size-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip.right="{
|
v-tooltip.right="{
|
||||||
value: t('sideToolbar.labels.menu'),
|
value: t('sideToolbar.mediaAssets.title'),
|
||||||
...tooltipOptions
|
...tooltipOptions
|
||||||
}"
|
}"
|
||||||
variant="secondary"
|
variant="textonly"
|
||||||
size="unset"
|
size="unset"
|
||||||
:aria-label="t('sideToolbar.labels.menu')"
|
:aria-label="t('sideToolbar.mediaAssets.title')"
|
||||||
class="relative h-10 gap-1 rounded-lg pr-2 pl-3 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
:class="
|
||||||
|
cn('size-10', isAssetsActive && 'bg-secondary-background-hover')
|
||||||
|
"
|
||||||
|
@click="openAssets"
|
||||||
|
>
|
||||||
|
<i class="icon-[comfy--image-ai-edit] size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('linearMode.appModeToolbar.apps'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="textonly"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('linearMode.appModeToolbar.apps')"
|
||||||
|
:class="
|
||||||
|
cn('size-10', isAppsActive && 'bg-secondary-background-hover')
|
||||||
|
"
|
||||||
|
@click="showApps"
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--panels-top-left] size-4" />
|
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||||
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
|
|
||||||
<span
|
|
||||||
v-if="hasUnseenItems"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</div>
|
||||||
</WorkflowActionsDropdown>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
v-if="enableAppBuilder"
|
|
||||||
v-tooltip.right="{
|
|
||||||
value: t('linearMode.appModeToolbar.appBuilder'),
|
|
||||||
...tooltipOptions
|
|
||||||
}"
|
|
||||||
variant="secondary"
|
|
||||||
size="unset"
|
|
||||||
:disabled="!hasNodes"
|
|
||||||
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
|
|
||||||
class="size-10 rounded-lg"
|
|
||||||
@click="enterBuilder"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--hammer] size-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
v-tooltip.right="{
|
|
||||||
value: t('sideToolbar.mediaAssets.title'),
|
|
||||||
...tooltipOptions
|
|
||||||
}"
|
|
||||||
variant="textonly"
|
|
||||||
size="unset"
|
|
||||||
:aria-label="t('sideToolbar.mediaAssets.title')"
|
|
||||||
:class="
|
|
||||||
cn('size-10', isAssetsActive && 'bg-secondary-background-hover')
|
|
||||||
"
|
|
||||||
@click="openAssets"
|
|
||||||
>
|
|
||||||
<i class="icon-[comfy--image-ai-edit] size-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-tooltip.right="{
|
|
||||||
value: t('linearMode.appModeToolbar.apps'),
|
|
||||||
...tooltipOptions
|
|
||||||
}"
|
|
||||||
variant="textonly"
|
|
||||||
size="unset"
|
|
||||||
:aria-label="t('linearMode.appModeToolbar.apps')"
|
|
||||||
:class="cn('size-10', isAppsActive && 'bg-secondary-background-hover')"
|
|
||||||
@click="showApps"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--panels-top-left] size-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-tooltip.right="{
|
|
||||||
value: t('sideToolbar.templates'),
|
|
||||||
...tooltipOptions
|
|
||||||
}"
|
|
||||||
variant="textonly"
|
|
||||||
size="unset"
|
|
||||||
:aria-label="t('sideToolbar.templates')"
|
|
||||||
class="size-10"
|
|
||||||
@click="openTemplates"
|
|
||||||
>
|
|
||||||
<i class="icon-[comfy--template] size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<WorkflowActionsDropdown source="app_mode_toolbar" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
data-testid="subgraph-breadcrumb"
|
data-testid="subgraph-breadcrumb"
|
||||||
class="subgraph-breadcrumb -mt-4 flex w-auto items-center pt-4 drop-shadow-(--interface-panel-drop-shadow)"
|
class="subgraph-breadcrumb -mt-3 flex w-auto items-center pt-4 pl-1 drop-shadow-(--interface-panel-drop-shadow)"
|
||||||
:class="{
|
:class="{
|
||||||
'subgraph-breadcrumb-collapse': collapseTabs,
|
'subgraph-breadcrumb-collapse': collapseTabs,
|
||||||
'subgraph-breadcrumb-overflow': overflowingTabs
|
'subgraph-breadcrumb-overflow': overflowingTabs
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
|||||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||||
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
import {
|
||||||
|
LGraphEventMode,
|
||||||
|
TitleMode
|
||||||
|
} from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -24,7 +27,7 @@ import { app } from '@/scripts/app'
|
|||||||
import { DOMWidgetImpl } from '@/scripts/domWidget'
|
import { DOMWidgetImpl } from '@/scripts/domWidget'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useAppMode } from '@/composables/useAppMode'
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
import { useAppModeStore } from '@/stores/appModeStore'
|
import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore'
|
||||||
import { resolveNode } from '@/utils/litegraphUtil'
|
import { resolveNode } from '@/utils/litegraphUtil'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
||||||
@@ -159,7 +162,12 @@ function handleDown(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
const [node, widget] = getHovered(e) ?? []
|
const [node, widget] = getHovered(e) ?? []
|
||||||
if (!node) return canvasInteractions.forwardEventToCanvas(e)
|
if (
|
||||||
|
node?.mode !== LGraphEventMode.ALWAYS ||
|
||||||
|
!nodeTypeValidForApp(node.type) ||
|
||||||
|
node.has_errors
|
||||||
|
)
|
||||||
|
return canvasInteractions.forwardEventToCanvas(e)
|
||||||
|
|
||||||
if (!widget) {
|
if (!widget) {
|
||||||
if (!isSelectOutputsMode.value) return
|
if (!isSelectOutputsMode.value) return
|
||||||
@@ -192,7 +200,12 @@ function nodeToDisplayTuple(
|
|||||||
const renderedOutputs = computed(() => {
|
const renderedOutputs = computed(() => {
|
||||||
void appModeStore.selectedOutputs.length
|
void appModeStore.selectedOutputs.length
|
||||||
return canvas
|
return canvas
|
||||||
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
|
.graph!.nodes.filter(
|
||||||
|
(n) =>
|
||||||
|
n.constructor.nodeData?.output_node &&
|
||||||
|
n.mode === LGraphEventMode.ALWAYS &&
|
||||||
|
!n.has_errors
|
||||||
|
)
|
||||||
.map(nodeToDisplayTuple)
|
.map(nodeToDisplayTuple)
|
||||||
})
|
})
|
||||||
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||||
@@ -204,131 +217,152 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center border-b border-border-subtle p-2 font-bold">
|
<div class="flex h-full flex-col">
|
||||||
{{
|
<div class="flex items-center border-b border-border-subtle p-2 font-bold">
|
||||||
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
|
{{
|
||||||
}}
|
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
|
||||||
</div>
|
}}
|
||||||
<DraggableList
|
</div>
|
||||||
v-if="isArrangeMode"
|
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||||
v-slot="{ dragClass }"
|
<DraggableList
|
||||||
v-model="appModeStore.selectedInputs"
|
v-if="isArrangeMode"
|
||||||
>
|
v-slot="{ dragClass }"
|
||||||
<div
|
v-model="appModeStore.selectedInputs"
|
||||||
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
|
class="overflow-x-clip"
|
||||||
:key="`${nodeId}: ${widgetName}`"
|
>
|
||||||
:class="cn(dragClass, 'pointer-events-auto my-2 p-2')"
|
<div
|
||||||
:aria-label="`${widget?.label ?? widgetName} — ${node.title}`"
|
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
|
||||||
>
|
:key="`${nodeId}: ${widgetName}`"
|
||||||
<div v-if="widget" class="pointer-events-none" inert>
|
:class="cn(dragClass, 'pointer-events-auto my-2 p-2')"
|
||||||
<WidgetItem
|
:aria-label="`${widget?.label ?? widgetName} — ${node.title}`"
|
||||||
:widget="widget"
|
>
|
||||||
:node="node"
|
<div v-if="widget" class="pointer-events-none" inert>
|
||||||
show-node-name
|
<WidgetItem
|
||||||
hidden-widget-actions
|
:widget="widget"
|
||||||
/>
|
:node="node"
|
||||||
|
show-node-name
|
||||||
|
hidden-widget-actions
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="pointer-events-none p-1 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ widgetName }}
|
||||||
|
<p class="text-xs italic">
|
||||||
|
({{ t('linearMode.builder.unknownWidget') }})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DraggableList>
|
||||||
|
<PropertiesAccordionItem
|
||||||
|
v-if="isSelectInputsMode"
|
||||||
|
:label="t('nodeHelpPage.inputs')"
|
||||||
|
enable-empty-state
|
||||||
|
:disabled="!appModeStore.selectedInputs.length"
|
||||||
|
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
||||||
|
:tooltip-delay="100"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
{{ t('nodeHelpPage.inputs') }}
|
||||||
|
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<div
|
||||||
|
class="p-4 text-muted-foreground"
|
||||||
|
v-text="t('linearMode.builder.promptAddInputs')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<DraggableList
|
||||||
|
v-slot="{ dragClass }"
|
||||||
|
v-model="appModeStore.selectedInputs"
|
||||||
|
>
|
||||||
|
<IoItem
|
||||||
|
v-for="{
|
||||||
|
nodeId,
|
||||||
|
widgetName,
|
||||||
|
label,
|
||||||
|
subLabel,
|
||||||
|
rename
|
||||||
|
} in inputsWithState"
|
||||||
|
:key="`${nodeId}: ${widgetName}`"
|
||||||
|
:class="
|
||||||
|
cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')
|
||||||
|
"
|
||||||
|
:title="label ?? widgetName"
|
||||||
|
:sub-title="subLabel"
|
||||||
|
:rename
|
||||||
|
:remove="
|
||||||
|
() =>
|
||||||
|
remove(
|
||||||
|
appModeStore.selectedInputs,
|
||||||
|
([id, name]) => nodeId == id && widgetName === name
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</DraggableList>
|
||||||
|
</PropertiesAccordionItem>
|
||||||
|
<div
|
||||||
|
v-if="isSelectInputsMode && !appModeStore.selectedInputs.length"
|
||||||
|
class="m-4 flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-primary-background bg-primary-background/20 text-center text-sm text-primary-background"
|
||||||
|
>
|
||||||
|
{{ t('linearMode.builder.inputPlaceholder') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="pointer-events-none p-1 text-sm text-muted-foreground">
|
<PropertiesAccordionItem
|
||||||
{{ widgetName }}
|
v-if="isSelectOutputsMode"
|
||||||
<p class="text-xs italic">
|
:label="t('nodeHelpPage.outputs')"
|
||||||
({{ t('linearMode.builder.unknownWidget') }})
|
enable-empty-state
|
||||||
</p>
|
:disabled="!appModeStore.selectedOutputs.length"
|
||||||
|
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
||||||
|
:tooltip-delay="100"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
{{ t('nodeHelpPage.outputs') }}
|
||||||
|
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<div
|
||||||
|
class="p-4 text-muted-foreground"
|
||||||
|
v-text="t('linearMode.builder.promptAddOutputs')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<DraggableList
|
||||||
|
v-slot="{ dragClass }"
|
||||||
|
v-model="appModeStore.selectedOutputs"
|
||||||
|
>
|
||||||
|
<IoItem
|
||||||
|
v-for="([key, title], index) in outputsWithState"
|
||||||
|
:key
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
dragClass,
|
||||||
|
'my-2 rounded-lg bg-warning-background/40 p-2',
|
||||||
|
index === 0 && 'ring-2 ring-warning-background'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:title
|
||||||
|
:sub-title="String(key)"
|
||||||
|
:remove="
|
||||||
|
() => remove(appModeStore.selectedOutputs, (k) => k == key)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</DraggableList>
|
||||||
|
</PropertiesAccordionItem>
|
||||||
|
<div
|
||||||
|
v-if="isSelectOutputsMode && !appModeStore.selectedOutputs.length"
|
||||||
|
class="m-4 flex flex-1 flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed border-warning-background bg-warning-background/20 text-center text-sm text-warning-background"
|
||||||
|
>
|
||||||
|
{{ t('linearMode.builder.outputPlaceholder') }}
|
||||||
|
<span class="font-bold">
|
||||||
|
{{ t('linearMode.builder.outputRequiredPlaceholder') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DraggableList>
|
</div>
|
||||||
<PropertiesAccordionItem
|
|
||||||
v-if="isSelectInputsMode"
|
|
||||||
:label="t('nodeHelpPage.inputs')"
|
|
||||||
enable-empty-state
|
|
||||||
:disabled="!appModeStore.selectedInputs.length"
|
|
||||||
class="border-b border-border-subtle"
|
|
||||||
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
|
||||||
:tooltip-delay="100"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
{{ t('nodeHelpPage.inputs') }}
|
|
||||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 pt-2 text-muted-foreground"
|
|
||||||
v-text="t('linearMode.builder.promptAddInputs')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 pt-2 text-muted-foreground"
|
|
||||||
v-text="t('linearMode.builder.promptAddInputs')"
|
|
||||||
/>
|
|
||||||
<DraggableList v-slot="{ dragClass }" v-model="appModeStore.selectedInputs">
|
|
||||||
<IoItem
|
|
||||||
v-for="{
|
|
||||||
nodeId,
|
|
||||||
widgetName,
|
|
||||||
label,
|
|
||||||
subLabel,
|
|
||||||
rename
|
|
||||||
} in inputsWithState"
|
|
||||||
:key="`${nodeId}: ${widgetName}`"
|
|
||||||
:class="cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')"
|
|
||||||
:title="label ?? widgetName"
|
|
||||||
:sub-title="subLabel"
|
|
||||||
:rename
|
|
||||||
:remove="
|
|
||||||
() =>
|
|
||||||
remove(
|
|
||||||
appModeStore.selectedInputs,
|
|
||||||
([id, name]) => nodeId == id && widgetName === name
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</DraggableList>
|
|
||||||
</PropertiesAccordionItem>
|
|
||||||
<PropertiesAccordionItem
|
|
||||||
v-if="isSelectOutputsMode"
|
|
||||||
:label="t('nodeHelpPage.outputs')"
|
|
||||||
enable-empty-state
|
|
||||||
:disabled="!appModeStore.selectedOutputs.length"
|
|
||||||
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
|
||||||
:tooltip-delay="100"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
{{ t('nodeHelpPage.outputs') }}
|
|
||||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #empty>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 pt-2 text-muted-foreground"
|
|
||||||
v-text="t('linearMode.builder.promptAddOutputs')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
class="w-full p-4 pt-2 text-muted-foreground"
|
|
||||||
v-text="t('linearMode.builder.promptAddOutputs')"
|
|
||||||
/>
|
|
||||||
<DraggableList
|
|
||||||
v-slot="{ dragClass }"
|
|
||||||
v-model="appModeStore.selectedOutputs"
|
|
||||||
>
|
|
||||||
<IoItem
|
|
||||||
v-for="([key, title], index) in outputsWithState"
|
|
||||||
:key
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
dragClass,
|
|
||||||
'my-2 rounded-lg bg-warning-background/40 p-2',
|
|
||||||
index === 0 && 'ring-2 ring-warning-background'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:title
|
|
||||||
:sub-title="String(key)"
|
|
||||||
:remove="() => remove(appModeStore.selectedOutputs, (k) => k == key)"
|
|
||||||
/>
|
|
||||||
</DraggableList>
|
|
||||||
</PropertiesAccordionItem>
|
|
||||||
|
|
||||||
<Teleport
|
<Teleport
|
||||||
v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
|
v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
|
||||||
|
|||||||
@@ -38,8 +38,8 @@
|
|||||||
<Button variant="muted-textonly" size="lg" @click="$emit('viewApp')">
|
<Button variant="muted-textonly" size="lg" @click="$emit('viewApp')">
|
||||||
{{ $t('builderToolbar.viewApp') }}
|
{{ $t('builderToolbar.viewApp') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="lg" @click="$emit('close')">
|
<Button variant="secondary" size="lg" @click="$emit('exitToWorkflow')">
|
||||||
{{ $t('g.close') }}
|
{{ $t('builderToolbar.exitToWorkflow') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,5 +58,6 @@ defineProps<{
|
|||||||
defineEmits<{
|
defineEmits<{
|
||||||
viewApp: []
|
viewApp: []
|
||||||
close: []
|
close: []
|
||||||
|
exitToWorkflow: []
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ useEventListener(window, 'keydown', (e: KeyboardEvent) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function onExitBuilder() {
|
function onExitBuilder() {
|
||||||
void appModeStore.exitBuilder()
|
appModeStore.exitBuilder()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -19,38 +19,31 @@
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ close }">
|
<template #default="{ close }">
|
||||||
<button
|
<template v-for="(item, index) in menuItems" :key="item.label">
|
||||||
:class="
|
<div v-if="index > 0" class="my-1 border-t border-border-default" />
|
||||||
cn(
|
<Button
|
||||||
'flex w-full items-center gap-3 rounded-md border-none bg-transparent px-3 py-2 text-sm',
|
variant="textonly"
|
||||||
hasOutputs
|
size="unset"
|
||||||
? 'cursor-pointer hover:bg-secondary-background-hover'
|
class="flex w-full items-center justify-start gap-3 rounded-md px-3 py-2 text-sm"
|
||||||
: 'pointer-events-none opacity-50'
|
:disabled="item.disabled"
|
||||||
)
|
@click="item.action(close)"
|
||||||
"
|
>
|
||||||
:disabled="!hasOutputs"
|
<i :class="cn(item.icon, 'size-4')" />
|
||||||
@click="onSave(close)"
|
{{ item.label }}
|
||||||
>
|
</Button>
|
||||||
<i class="icon-[lucide--save] size-4" />
|
</template>
|
||||||
{{ t('g.save') }}
|
|
||||||
</button>
|
|
||||||
<div class="my-1 border-t border-border-default" />
|
|
||||||
<button
|
|
||||||
class="flex w-full cursor-pointer items-center gap-3 rounded-md border-none bg-transparent px-3 py-2 text-sm hover:bg-secondary-background-hover"
|
|
||||||
@click="onExitBuilder(close)"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--square-pen] size-4" />
|
|
||||||
{{ t('builderMenu.exitAppBuilder') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import Popover from '@/components/ui/Popover.vue'
|
import Popover from '@/components/ui/Popover.vue'
|
||||||
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
@@ -60,10 +53,30 @@ import { cn } from '@/utils/tailwindUtil'
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appModeStore = useAppModeStore()
|
const appModeStore = useAppModeStore()
|
||||||
const { hasOutputs } = storeToRefs(appModeStore)
|
const { hasOutputs } = storeToRefs(appModeStore)
|
||||||
|
const { setMode } = useAppMode()
|
||||||
const workflowService = useWorkflowService()
|
const workflowService = useWorkflowService()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const { toastErrorHandler } = useErrorHandling()
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
|
|
||||||
|
const menuItems = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('g.save'),
|
||||||
|
icon: 'icon-[lucide--save]',
|
||||||
|
disabled: !hasOutputs.value,
|
||||||
|
action: onSave
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('builderMenu.enterAppMode'),
|
||||||
|
icon: 'icon-[lucide--panels-top-left]',
|
||||||
|
action: onEnterAppMode
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('builderMenu.exitAppBuilder'),
|
||||||
|
icon: 'icon-[lucide--x]',
|
||||||
|
action: onExitBuilder
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
async function onSave(close: () => void) {
|
async function onSave(close: () => void) {
|
||||||
const workflow = workflowStore.activeWorkflow
|
const workflow = workflowStore.activeWorkflow
|
||||||
if (!workflow) return
|
if (!workflow) return
|
||||||
@@ -75,8 +88,13 @@ async function onSave(close: () => void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEnterAppMode(close: () => void) {
|
||||||
|
setMode('app')
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
function onExitBuilder(close: () => void) {
|
function onExitBuilder(close: () => void) {
|
||||||
void appModeStore.exitBuilder()
|
appModeStore.exitBuilder()
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import Popover from '@/components/ui/Popover.vue'
|
import Popover from '@/components/ui/Popover.vue'
|
||||||
@@ -7,6 +7,13 @@ import Button from '@/components/ui/button/Button.vue'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const titleTooltip = ref<string | null>(null)
|
||||||
|
const subTitleTooltip = ref<string | null>(null)
|
||||||
|
|
||||||
|
function isTruncated(e: MouseEvent): boolean {
|
||||||
|
const el = e.currentTarget as HTMLElement
|
||||||
|
return el.scrollWidth > el.clientWidth
|
||||||
|
}
|
||||||
const { rename, remove } = defineProps<{
|
const { rename, remove } = defineProps<{
|
||||||
title: string
|
title: string
|
||||||
subTitle?: string
|
subTitle?: string
|
||||||
@@ -32,15 +39,28 @@ const entries = computed(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="my-2 flex items-center-safe gap-2 rounded-lg p-2">
|
<div
|
||||||
<div
|
class="my-2 flex items-center-safe gap-2 rounded-lg p-2"
|
||||||
class="drag-handle mr-auto inline max-w-max min-w-0 flex-[4_1_0%] truncate"
|
data-testid="builder-io-item"
|
||||||
v-text="title"
|
>
|
||||||
/>
|
<div class="drag-handle mr-auto flex min-w-0 flex-col gap-1">
|
||||||
<div
|
<div
|
||||||
class="drag-handle inline max-w-max min-w-0 flex-[2_1_0%] truncate text-end text-muted-foreground"
|
v-tooltip.left="{ value: titleTooltip, showDelay: 300 }"
|
||||||
v-text="subTitle"
|
class="drag-handle truncate text-sm"
|
||||||
/>
|
data-testid="builder-io-item-title"
|
||||||
|
@mouseenter="titleTooltip = isTruncated($event) ? title : null"
|
||||||
|
v-text="title"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-tooltip.left="{ value: subTitleTooltip, showDelay: 300 }"
|
||||||
|
class="drag-handle truncate text-xs text-muted-foreground"
|
||||||
|
data-testid="builder-io-item-subtitle"
|
||||||
|
@mouseenter="
|
||||||
|
subTitleTooltip = isTruncated($event) ? (subTitle ?? null) : null
|
||||||
|
"
|
||||||
|
v-text="subTitle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Popover :entries>
|
<Popover :entries>
|
||||||
<template #button>
|
<template #button>
|
||||||
<Button variant="muted-textonly">
|
<Button variant="muted-textonly">
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ const mockApp = vi.hoisted(() => ({
|
|||||||
|
|
||||||
const mockSetMode = vi.hoisted(() => vi.fn())
|
const mockSetMode = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
|
const mockAppModeStore = vi.hoisted(() => ({
|
||||||
|
exitBuilder: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/services/dialogService', () => ({
|
vi.mock('@/services/dialogService', () => ({
|
||||||
useDialogService: () => mockDialogService
|
useDialogService: () => mockDialogService
|
||||||
}))
|
}))
|
||||||
@@ -42,6 +46,10 @@ vi.mock('@/composables/useAppMode', () => ({
|
|||||||
useAppMode: () => ({ setMode: mockSetMode })
|
useAppMode: () => ({ setMode: mockSetMode })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/appModeStore', () => ({
|
||||||
|
useAppModeStore: () => mockAppModeStore
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('./DefaultViewDialogContent.vue', () => ({
|
vi.mock('./DefaultViewDialogContent.vue', () => ({
|
||||||
default: { name: 'MockDefaultViewDialogContent' }
|
default: { name: 'MockDefaultViewDialogContent' }
|
||||||
}))
|
}))
|
||||||
@@ -208,6 +216,16 @@ describe('useAppSetDefaultView', () => {
|
|||||||
expect(mockSetMode).toHaveBeenCalledWith('app')
|
expect(mockSetMode).toHaveBeenCalledWith('app')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('onExitToWorkflow exits builder and closes dialog', () => {
|
||||||
|
const confirmCall = applyAndGetConfirmDialog(true)
|
||||||
|
confirmCall.props.onExitToWorkflow()
|
||||||
|
|
||||||
|
expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({
|
||||||
|
key: 'builder-default-view-applied'
|
||||||
|
})
|
||||||
|
expect(mockAppModeStore.exitBuilder).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
it('onClose closes confirmation dialog', () => {
|
it('onClose closes confirmation dialog', () => {
|
||||||
const confirmCall = applyAndGetConfirmDialog(true)
|
const confirmCall = applyAndGetConfirmDialog(true)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useDialogStore } from '@/stores/dialogStore'
|
|||||||
|
|
||||||
import BuilderDefaultModeAppliedDialogContent from './BuilderDefaultModeAppliedDialogContent.vue'
|
import BuilderDefaultModeAppliedDialogContent from './BuilderDefaultModeAppliedDialogContent.vue'
|
||||||
import DefaultViewDialogContent from './DefaultViewDialogContent.vue'
|
import DefaultViewDialogContent from './DefaultViewDialogContent.vue'
|
||||||
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
|
|
||||||
const DIALOG_KEY = 'builder-default-view'
|
const DIALOG_KEY = 'builder-default-view'
|
||||||
const APPLIED_DIALOG_KEY = 'builder-default-view-applied'
|
const APPLIED_DIALOG_KEY = 'builder-default-view-applied'
|
||||||
@@ -16,6 +17,7 @@ export function useAppSetDefaultView() {
|
|||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
const appModeStore = useAppModeStore()
|
||||||
const { setMode } = useAppMode()
|
const { setMode } = useAppMode()
|
||||||
|
|
||||||
const settingView = computed(() => dialogStore.isDialogOpen(DIALOG_KEY))
|
const settingView = computed(() => dialogStore.isDialogOpen(DIALOG_KEY))
|
||||||
@@ -54,6 +56,10 @@ export function useAppSetDefaultView() {
|
|||||||
closeAppliedDialog()
|
closeAppliedDialog()
|
||||||
setMode('app')
|
setMode('app')
|
||||||
},
|
},
|
||||||
|
onExitToWorkflow: () => {
|
||||||
|
closeAppliedDialog()
|
||||||
|
appModeStore.exitBuilder()
|
||||||
|
},
|
||||||
onClose: closeAppliedDialog
|
onClose: closeAppliedDialog
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
:variant="buttonVariant ?? 'textonly'"
|
:variant="buttonVariant ?? 'textonly'"
|
||||||
@click="$emit('action')"
|
@click="$emit('action')"
|
||||||
>
|
>
|
||||||
|
<i v-if="buttonIcon" :class="buttonIcon" />
|
||||||
{{ buttonLabel }}
|
{{ buttonLabel }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,6 +38,7 @@ const props = defineProps<{
|
|||||||
title?: string
|
title?: string
|
||||||
message: string
|
message: string
|
||||||
textClass?: string
|
textClass?: string
|
||||||
|
buttonIcon?: string
|
||||||
buttonLabel?: string
|
buttonLabel?: string
|
||||||
buttonVariant?: ButtonVariants['variant']
|
buttonVariant?: ButtonVariants['variant']
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Avatar
|
<Avatar
|
||||||
class="bg-interface-panel-selected-surface"
|
class="aspect-square bg-interface-panel-selected-surface"
|
||||||
:image="photoUrl ?? undefined"
|
:image="photoUrl ?? undefined"
|
||||||
:icon="hasAvatar ? undefined : 'icon-[lucide--user]'"
|
:icon="hasAvatar ? undefined : 'icon-[lucide--user]'"
|
||||||
:pt:icon:class="{ 'size-4': !hasAvatar }"
|
:pt:icon:class="{ 'size-4': !hasAvatar }"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
DropdownMenuRoot,
|
DropdownMenuRoot,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from 'reka-ui'
|
} from 'reka-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import WorkflowActionsList from '@/components/common/WorkflowActionsList.vue'
|
import WorkflowActionsList from '@/components/common/WorkflowActionsList.vue'
|
||||||
@@ -22,6 +23,7 @@ const { source, align = 'start' } = defineProps<{
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
const dropdownOpen = ref(false)
|
||||||
|
|
||||||
const { menuItems } = useWorkflowActionsMenu(
|
const { menuItems } = useWorkflowActionsMenu(
|
||||||
() => useCommandStore().execute('Comfy.RenameWorkflow'),
|
() => useCommandStore().execute('Comfy.RenameWorkflow'),
|
||||||
@@ -40,22 +42,48 @@ function handleOpen(open: boolean) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleLinearMode() {
|
||||||
|
dropdownOpen.value = false
|
||||||
|
void useCommandStore().execute('Comfy.ToggleLinear', {
|
||||||
|
metadata: { source }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tooltipPt = {
|
||||||
|
root: {
|
||||||
|
style: { transform: 'translateX(calc(50% - 16px))' }
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
class: '!left-[16px]'
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DropdownMenuRoot @update:open="handleOpen">
|
<DropdownMenuRoot v-model:open="dropdownOpen" @update:open="handleOpen">
|
||||||
<DropdownMenuTrigger as-child>
|
<slot name="button" :has-unseen-items="hasUnseenItems">
|
||||||
<slot name="button" :has-unseen-items="hasUnseenItems">
|
<div
|
||||||
|
class="pointer-events-auto inline-flex items-center rounded-lg bg-secondary-background"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip="{
|
v-tooltip.bottom="{
|
||||||
value: t('breadcrumbsMenu.workflowActions'),
|
value: canvasStore.linearMode
|
||||||
|
? t('breadcrumbsMenu.enterNodeGraph')
|
||||||
|
: t('breadcrumbsMenu.enterAppMode'),
|
||||||
showDelay: 300,
|
showDelay: 300,
|
||||||
hideDelay: 300
|
hideDelay: 300,
|
||||||
|
pt: tooltipPt
|
||||||
}"
|
}"
|
||||||
variant="secondary"
|
:aria-label="
|
||||||
size="unset"
|
canvasStore.linearMode
|
||||||
:aria-label="t('breadcrumbsMenu.workflowActions')"
|
? t('breadcrumbsMenu.enterNodeGraph')
|
||||||
class="pointer-events-auto relative h-10 gap-1 rounded-lg pr-2 pl-3 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
: t('breadcrumbsMenu.enterAppMode')
|
||||||
|
"
|
||||||
|
variant="base"
|
||||||
|
class="m-1"
|
||||||
|
@pointerdown.stop
|
||||||
|
@click="toggleLinearMode"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
class="size-4"
|
class="size-4"
|
||||||
@@ -65,15 +93,36 @@ function handleOpen(open: boolean) {
|
|||||||
: 'icon-[comfy--workflow]'
|
: 'icon-[comfy--workflow]'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
|
|
||||||
<span
|
|
||||||
v-if="hasUnseenItems"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</slot>
|
<DropdownMenuTrigger as-child>
|
||||||
</DropdownMenuTrigger>
|
<Button
|
||||||
|
v-tooltip="{
|
||||||
|
value: t('breadcrumbsMenu.workflowActions'),
|
||||||
|
showDelay: 300,
|
||||||
|
hideDelay: 300
|
||||||
|
}"
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('breadcrumbsMenu.workflowActions')"
|
||||||
|
class="relative h-10 gap-1 rounded-lg pr-2 pl-2.5 text-center data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
||||||
|
>
|
||||||
|
<span>{{
|
||||||
|
canvasStore.linearMode
|
||||||
|
? t('breadcrumbsMenu.app')
|
||||||
|
: t('breadcrumbsMenu.graph')
|
||||||
|
}}</span>
|
||||||
|
<i
|
||||||
|
class="icon-[lucide--chevron-down] size-4 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
v-if="hasUnseenItems"
|
||||||
|
aria-hidden="true"
|
||||||
|
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
:align
|
:align
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex flex-col border-t border-border-default px-4 py-2 text-sm wrap-break-word text-muted-foreground"
|
class="flex flex-col border-t border-border-default px-4 py-2 text-sm wrap-break-word text-muted-foreground"
|
||||||
>
|
>
|
||||||
<p v-if="promptTextReal">
|
<p v-if="promptTextReal" :class="preserveNewlines && 'whitespace-pre-line'">
|
||||||
{{ promptTextReal }}
|
{{ promptTextReal }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
import { computed, toValue } from 'vue'
|
import { computed, toValue } from 'vue'
|
||||||
import type { MaybeRefOrGetter } from 'vue'
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
|
||||||
const { promptText } = defineProps<{
|
const { promptText, preserveNewlines = false } = defineProps<{
|
||||||
promptText?: MaybeRefOrGetter<string>
|
promptText?: MaybeRefOrGetter<string>
|
||||||
|
preserveNewlines?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const promptTextReal = computed(() => toValue(promptText))
|
const promptTextReal = computed(() => toValue(promptText))
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
:disabled
|
:disabled
|
||||||
variant="textonly"
|
:variant="confirmVariant ?? 'textonly'"
|
||||||
:class="confirmClass"
|
:class="confirmClass"
|
||||||
@click="$emit('confirm')"
|
@click="$emit('confirm')"
|
||||||
>
|
>
|
||||||
@@ -19,13 +19,21 @@ import type { MaybeRefOrGetter } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import type { ButtonVariants } from '@/components/ui/button/button.variants'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const { cancelText, confirmText, confirmClass, optionsDisabled } = defineProps<{
|
const {
|
||||||
|
cancelText,
|
||||||
|
confirmText,
|
||||||
|
confirmClass,
|
||||||
|
confirmVariant,
|
||||||
|
optionsDisabled
|
||||||
|
} = defineProps<{
|
||||||
cancelText?: string
|
cancelText?: string
|
||||||
confirmText?: string
|
confirmText?: string
|
||||||
confirmClass?: string
|
confirmClass?: string
|
||||||
|
confirmVariant?: ButtonVariants['variant']
|
||||||
optionsDisabled?: MaybeRefOrGetter<boolean>
|
optionsDisabled?: MaybeRefOrGetter<boolean>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -138,8 +138,7 @@ onMounted(async () => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('g.error'),
|
summary: t('g.error'),
|
||||||
detail: t('toastMessages.failedToFetchLogs'),
|
detail: t('toastMessages.failedToFetchLogs')
|
||||||
life: 5000
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -275,8 +275,7 @@ async function handleBuy() {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('credits.topUp.purchaseError'),
|
summary: t('credits.topUp.purchaseError'),
|
||||||
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
|
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage })
|
||||||
life: 5000
|
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
|||||||
@@ -98,8 +98,7 @@ async function onConfirmCancel() {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('subscription.cancelDialog.failed'),
|
summary: t('subscription.cancelDialog.failed'),
|
||||||
detail: error instanceof Error ? error.message : t('g.unknownError'),
|
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||||
life: 5000
|
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
|||||||
@@ -579,8 +579,7 @@ const onUpdateComfyUI = async (): Promise<void> => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('g.error'),
|
summary: t('g.error'),
|
||||||
detail: error.value || t('helpCenter.updateComfyUIFailed'),
|
detail: error.value || t('helpCenter.updateComfyUIFailed')
|
||||||
life: 5000
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -597,8 +596,7 @@ const onUpdateComfyUI = async (): Promise<void> => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
summary: t('g.error'),
|
summary: t('g.error'),
|
||||||
detail: err instanceof Error ? err.message : t('g.unknownError'),
|
detail: err instanceof Error ? err.message : t('g.unknownError')
|
||||||
life: 5000
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,8 @@
|
|||||||
v-if="isHelpCenterVisible"
|
v-if="isHelpCenterVisible"
|
||||||
class="help-center-popup"
|
class="help-center-popup"
|
||||||
:class="{
|
:class="{
|
||||||
'sidebar-left':
|
'sidebar-left': sidebarLocation === 'left',
|
||||||
triggerLocation === 'sidebar' && sidebarLocation === 'left',
|
'sidebar-right': sidebarLocation === 'right',
|
||||||
'sidebar-right':
|
|
||||||
triggerLocation === 'sidebar' && sidebarLocation === 'right',
|
|
||||||
'topbar-right': triggerLocation === 'topbar',
|
|
||||||
'small-sidebar': isSmall
|
'small-sidebar': isSmall
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -63,7 +60,6 @@ const { isSmall = false } = defineProps<{
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
isHelpCenterVisible,
|
isHelpCenterVisible,
|
||||||
triggerLocation,
|
|
||||||
sidebarLocation,
|
sidebarLocation,
|
||||||
closeHelpCenter,
|
closeHelpCenter,
|
||||||
handleWhatsNewDismissed
|
handleWhatsNewDismissed
|
||||||
@@ -101,25 +97,6 @@ const {
|
|||||||
right: 1rem;
|
right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-center-popup.topbar-right {
|
|
||||||
top: 2rem;
|
|
||||||
right: 1rem;
|
|
||||||
bottom: auto;
|
|
||||||
animation: slideInDown 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideInUp {
|
@keyframes slideInUp {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||