Compare commits
30 Commits
fix/codera
...
drjkl/desl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d88d68c17 | ||
|
|
fbb31a13a2 | ||
|
|
e77f9d5cd9 | ||
|
|
425e06a07e | ||
|
|
62c88bf3d0 | ||
|
|
3c166ef654 | ||
|
|
64c3dac4e9 | ||
|
|
7f53451ab9 | ||
|
|
c72ab181b0 | ||
|
|
30f6260b97 | ||
|
|
3956fa68a8 | ||
|
|
376c32df05 | ||
|
|
4796154884 | ||
|
|
2205ead595 | ||
|
|
dad9b4e71e | ||
|
|
d11e82cfbe | ||
|
|
2f0e84100d | ||
|
|
2a46bbf420 | ||
|
|
335b4edc63 | ||
|
|
85572cf7aa | ||
|
|
9eb5e18a65 | ||
|
|
1cbfdbc0cb | ||
|
|
2efe71df89 | ||
|
|
cb49fdbda2 | ||
|
|
f25427c845 | ||
|
|
17093f9721 | ||
|
|
eb27404987 | ||
|
|
8799d4be07 | ||
|
|
82ba0028fb | ||
|
|
ca42569cb1 |
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Update electron types
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
25
.github/workflows/cloud-dispatch-build.yaml
vendored
@@ -13,8 +13,6 @@ on:
|
||||
branches:
|
||||
- 'cloud/*'
|
||||
- 'main'
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
@@ -25,31 +23,16 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
# Fork guard: prevent forks from dispatching to the cloud repo.
|
||||
# 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')
|
||||
# Fork guard: prevent forks from dispatching to the cloud repo
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build client 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: |
|
||||
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 \
|
||||
--arg ref "${REF}" \
|
||||
--arg branch "${BRANCH}" \
|
||||
--arg ref "${GITHUB_SHA}" \
|
||||
--arg branch "${GITHUB_REF_NAME}" \
|
||||
'{ref: $ref, branch: $branch}')"
|
||||
echo "json=${payload}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
|
||||
2
.github/workflows/pr-claude-review.yaml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies for analysis tools
|
||||
|
||||
2
.github/workflows/pr-perf-report.yaml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 22
|
||||
|
||||
- name: Download PR metadata
|
||||
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: '24.x'
|
||||
|
||||
- name: Read desktop-ui version
|
||||
id: get_version
|
||||
|
||||
2
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -91,7 +91,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: '24.x'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: 'frontend/.nvmrc'
|
||||
node-version: lts/*
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
|
||||
2
.github/workflows/release-branch-create.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Check version bump type
|
||||
id: check_version
|
||||
|
||||
2
.github/workflows/release-draft-create.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get current version
|
||||
|
||||
2
.github/workflows/release-npm-types.yaml
vendored
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
|
||||
2
.github/workflows/release-pypi-dev.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Get current version
|
||||
|
||||
2
.github/workflows/release-version-bump.yaml
vendored
@@ -149,7 +149,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: lts/*
|
||||
|
||||
- name: Bump version
|
||||
id: bump-version
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: '24.x'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Bump desktop-ui version
|
||||
|
||||
2
.github/workflows/weekly-docs-check.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies for analysis tools
|
||||
|
||||
3
.gitignore
vendored
@@ -98,4 +98,5 @@ vitest.config.*.timestamp*
|
||||
# Weekly docs check output
|
||||
/output.txt
|
||||
|
||||
.amp
|
||||
.amp
|
||||
.venv
|
||||
@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
|
||||
### Prerequisites & Technology Stack
|
||||
|
||||
- **Required Software**:
|
||||
- Node.js (see `.nvmrc`, currently v24) and pnpm
|
||||
- Node.js (v24) and pnpm
|
||||
- Git for version control
|
||||
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
|
||||
<div class="p-terminal size-full rounded-none p-2">
|
||||
<div ref="terminalEl" class="terminal-host h-full" />
|
||||
<div
|
||||
ref="rootEl"
|
||||
class="relative overflow-hidden h-full w-full bg-neutral-900"
|
||||
>
|
||||
<div class="p-terminal rounded-none h-full w-full p-2">
|
||||
<div ref="terminalEl" class="h-full terminal-host" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip.left="{
|
||||
@@ -13,7 +16,7 @@
|
||||
size="small"
|
||||
:class="
|
||||
cn('absolute top-2 right-8 transition-opacity', {
|
||||
'pointer-events-none opacity-0 select-none': !isHovered
|
||||
'opacity-0 pointer-events-none select-none': !isHovered
|
||||
})
|
||||
"
|
||||
:aria-label="tooltipText"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="mx-auto flex w-full max-w-3xl flex-col gap-8 select-none">
|
||||
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
|
||||
<!-- Installation Path Section -->
|
||||
<div class="flex grow flex-col gap-6 text-neutral-300">
|
||||
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
|
||||
<div class="grow flex flex-col gap-6 text-neutral-300">
|
||||
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
|
||||
{{ $t('install.locationPicker.title') }}
|
||||
</h2>
|
||||
|
||||
<p class="px-12 text-center text-neutral-400">
|
||||
<p class="text-center text-neutral-400 px-12">
|
||||
{{ $t('install.locationPicker.subtitle') }}
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<InputText
|
||||
v-model="installPath"
|
||||
:placeholder="$t('install.locationPicker.pathPlaceholder')"
|
||||
class="flex-1 border-neutral-700 bg-neutral-800/50 text-neutral-200 placeholder:text-neutral-500"
|
||||
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
|
||||
:class="{ 'p-invalid': pathError }"
|
||||
@update:model-value="validatePath"
|
||||
@focus="onFocus"
|
||||
@@ -23,7 +23,7 @@
|
||||
<Button
|
||||
icon="pi pi-folder-open"
|
||||
severity="secondary"
|
||||
class="border-0 bg-neutral-700 hover:bg-neutral-600"
|
||||
class="bg-neutral-700 hover:bg-neutral-600 border-0"
|
||||
@click="browsePath"
|
||||
/>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
<Message
|
||||
v-if="pathError"
|
||||
severity="error"
|
||||
class="w-full whitespace-pre-line"
|
||||
class="whitespace-pre-line w-full"
|
||||
>
|
||||
{{ pathError }}
|
||||
</Message>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="size-full object-contain px-4 pt-4 opacity-25"
|
||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<i
|
||||
v-if="!isLoading && runner.state === 'OK'"
|
||||
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"
|
||||
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]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<template v-if="filter.tasks.length === 0">
|
||||
<!-- Empty filter -->
|
||||
<Divider />
|
||||
<p class="w-full text-center text-neutral-400">
|
||||
<p class="text-neutral-400 w-full text-center">
|
||||
{{ $t('maintenance.allOk') }}
|
||||
</p>
|
||||
</template>
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<!-- Display: Cards -->
|
||||
<template v-else>
|
||||
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
|
||||
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
|
||||
<TaskCard
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
@@ -45,8 +45,7 @@ import { useConfirm, useToast } from 'primevue'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type {
|
||||
MaintenanceFilter,
|
||||
@@ -56,7 +55,6 @@ import type {
|
||||
import TaskCard from './TaskCard.vue'
|
||||
import TaskListItem from './TaskListItem.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
@@ -82,7 +80,8 @@ const executeTask = async (task: MaintenanceTask) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('maintenance.error.toastTitle'),
|
||||
detail: message ?? t('maintenance.error.defaultDescription')
|
||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
||||
life: 10_000
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
export interface UVMirror {
|
||||
/**
|
||||
* The setting id defined for the mirror.
|
||||
*/
|
||||
settingId: string
|
||||
/**
|
||||
* The default mirror to use.
|
||||
*/
|
||||
mirror: string
|
||||
/**
|
||||
* The fallback mirror to use.
|
||||
*/
|
||||
fallbackMirror: string
|
||||
/**
|
||||
* The path suffix to validate the mirror is reachable.
|
||||
*/
|
||||
validationPathSuffix?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,33 @@
|
||||
import type { PrimeVueSeverity } from '../primeVueTypes'
|
||||
|
||||
interface MaintenanceTaskButton {
|
||||
/** The text to display on the button. */
|
||||
text?: string
|
||||
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
|
||||
/** CSS classes, e.g. 'pi pi-external-link' */
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/** A maintenance task, used by the maintenance page. */
|
||||
export interface MaintenanceTask {
|
||||
/** ID string used as i18n key */
|
||||
/** Used as i18n key */
|
||||
id: string
|
||||
/** The display name of the task, e.g. Git */
|
||||
name: string
|
||||
/** Short description of the task. */
|
||||
shortDescription?: string
|
||||
/** Description of the task when it is in an error state. */
|
||||
errorDescription?: string
|
||||
/** Description of the task when it is in a warning state. */
|
||||
warningDescription?: string
|
||||
/** Full description of the task when it is in an OK state. */
|
||||
description?: string
|
||||
/** URL to the image to show in card mode. */
|
||||
headerImg?: string
|
||||
/** The button to display on the task card / list item. */
|
||||
button?: MaintenanceTaskButton
|
||||
/** Whether to show a confirmation dialog before running the task. */
|
||||
requireConfirm?: boolean
|
||||
/** The text to display in the confirmation dialog. */
|
||||
confirmText?: string
|
||||
/** Called by onClick to run the actual task. */
|
||||
execute: (args?: unknown[]) => boolean | Promise<boolean>
|
||||
/** Show the button with `severity="danger"` */
|
||||
severity?: PrimeVueSeverity
|
||||
/** Whether this task should display the terminal window when run. */
|
||||
usesTerminal?: boolean
|
||||
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
|
||||
/** If true, successful completion refreshes install validation and auto-continues. */
|
||||
isInstallationFix?: boolean
|
||||
}
|
||||
|
||||
/** The filter options for the maintenance task list. */
|
||||
export interface MaintenanceFilter {
|
||||
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
|
||||
/** CSS classes, e.g. 'pi pi-cross' */
|
||||
icon: string
|
||||
/** The text to display on the filter button. */
|
||||
value: string
|
||||
/** The tasks to display when this filter is selected. */
|
||||
tasks: ReadonlyArray<MaintenanceTask>
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@ import { isValidUrl } from '@comfyorg/shared-frontend-utils/formatUtil'
|
||||
|
||||
import { electronAPI } from './envUtil'
|
||||
|
||||
/**
|
||||
* Check if a mirror is reachable from the electron App.
|
||||
* @param mirror - The mirror to check.
|
||||
* @returns True if the mirror is reachable, false otherwise.
|
||||
*/
|
||||
export const checkMirrorReachable = async (mirror: string) => {
|
||||
return (
|
||||
isValidUrl(mirror) && (await electronAPI().NetWork.canAccessUrl(mirror))
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
type ElectronWindow = typeof window & {
|
||||
electronAPI?: ElectronAPI
|
||||
}
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
}
|
||||
|
||||
export function electronAPI() {
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
export function electronAPI(): ElectronAPI {
|
||||
return (window as ElectronWindow).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
export function isNativeWindow() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex size-full flex-col justify-between rounded-lg p-6">
|
||||
<h1 class="m-0 font-inter text-xl font-semibold italic">
|
||||
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
|
||||
<h1 class="font-inter font-semibold text-xl m-0 italic">
|
||||
{{ $t(`desktopDialogs.${id}.title`, title) }}
|
||||
</h1>
|
||||
<p class="whitespace-pre-wrap">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div
|
||||
class="grid h-screen w-screen items-center justify-around overflow-y-auto"
|
||||
class="h-screen w-screen grid items-center justify-around overflow-y-auto"
|
||||
>
|
||||
<div class="relative m-8 text-center">
|
||||
<!-- Header -->
|
||||
@@ -13,7 +13,7 @@
|
||||
<span>{{ $t('desktopUpdate.description') }}</span>
|
||||
</div>
|
||||
|
||||
<ProgressSpinner class="m-8 size-48" />
|
||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
||||
|
||||
<!-- Console button -->
|
||||
<Button
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<!-- Fixed height container with flexbox layout for proper content management -->
|
||||
<div class="flex size-full flex-col">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<Stepper
|
||||
v-model:value="currentStep"
|
||||
class="flex h-full flex-col"
|
||||
class="flex flex-col h-full"
|
||||
@update:value="handleStepChange"
|
||||
>
|
||||
<!-- Main content area that grows to fill available space -->
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<!-- Install footer with navigation -->
|
||||
<InstallFooter
|
||||
class="mx-auto my-6 w-full max-w-2xl"
|
||||
class="w-full max-w-2xl my-6 mx-auto"
|
||||
:current-step
|
||||
:can-proceed
|
||||
:disable-location-step="noGpu"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<div class="relative m-8 w-screen max-w-(--breakpoint-sm)">
|
||||
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
|
||||
<!-- Header -->
|
||||
<h1 class="backspan pi-wrench text-4xl font-bold">
|
||||
{{ t('maintenance.title') }}
|
||||
</h1>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="flex w-full flex-wrap items-center gap-4">
|
||||
<div class="w-full flex flex-wrap gap-4 items-center">
|
||||
<span class="grow">
|
||||
{{ t('maintenance.status') }}:
|
||||
<StatusTag :refreshing="isRefreshing" :error="anyErrors" />
|
||||
</span>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<SelectButton
|
||||
v-model="displayAsList"
|
||||
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
|
||||
@@ -56,10 +56,10 @@
|
||||
:value="t('icon.exclamation-triangle')"
|
||||
/>
|
||||
<span>
|
||||
<strong class="mb-1 block">
|
||||
<strong class="block mb-1">
|
||||
{{ t('maintenance.unsafeMigration.title') }}
|
||||
</strong>
|
||||
<span class="mb-1 block">
|
||||
<span class="block mb-1">
|
||||
{{ unsafeReasonText }}
|
||||
</span>
|
||||
<span class="block text-sm text-neutral-400">
|
||||
@@ -71,13 +71,13 @@
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-x-0 border-y border-solid border-neutral-700"
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
:filter
|
||||
:display-as-list
|
||||
/>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex flex-row justify-between gap-4">
|
||||
<div class="flex justify-between gap-4 flex-row">
|
||||
<Button
|
||||
:label="t('maintenance.consoleLogs')"
|
||||
icon="pi pi-desktop"
|
||||
@@ -189,7 +189,8 @@ const completeValidation = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('maintenance.error.cannotContinue')
|
||||
detail: t('maintenance.error.cannotContinue'),
|
||||
life: 5_000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark hide-language-selector>
|
||||
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
|
||||
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
|
||||
<div
|
||||
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
|
||||
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
|
||||
>
|
||||
<h2 class="text-3xl font-semibold text-neutral-100">
|
||||
{{ $t('install.helpImprove') }}
|
||||
@@ -15,7 +15,7 @@
|
||||
<a
|
||||
href="https://comfy.org/privacy"
|
||||
target="_blank"
|
||||
class="text-blue-400 underline hover:text-blue-300"
|
||||
class="text-blue-400 hover:text-blue-300 underline"
|
||||
>
|
||||
{{ $t('install.privacyPolicy') }} </a
|
||||
>.
|
||||
@@ -33,7 +33,7 @@
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end pt-6">
|
||||
<div class="flex pt-6 justify-end">
|
||||
<Button
|
||||
:label="$t('g.ok')"
|
||||
icon="pi pi-check"
|
||||
@@ -72,7 +72,8 @@ const updateConsent = async () => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('install.settings.errorUpdatingConsent'),
|
||||
detail: t('install.settings.errorUpdatingConsentDetail')
|
||||
detail: t('install.settings.errorUpdatingConsentDetail'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/>
|
||||
|
||||
<div class="no-drag sad-text flex items-center">
|
||||
<div class="flex min-w-110 flex-col gap-8 p-8">
|
||||
<div class="flex flex-col gap-8 p-8 min-w-110">
|
||||
<!-- Header -->
|
||||
<h1 class="text-4xl font-bold text-red-500">
|
||||
{{ $t('notSupported.title') }}
|
||||
@@ -20,7 +20,7 @@
|
||||
<p class="text-xl">
|
||||
{{ $t('notSupported.message') }}
|
||||
</p>
|
||||
<ul class="list-inside list-disc space-y-1 text-neutral-800">
|
||||
<ul class="list-disc list-inside space-y-1 text-neutral-800">
|
||||
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
|
||||
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
|
||||
</ul>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<BaseViewTemplate dark>
|
||||
<div class="relative min-h-screen">
|
||||
<!-- Terminal Background Layer (always visible during loading) -->
|
||||
<div v-if="!isError" class="fixed inset-0 z-0 overflow-hidden">
|
||||
<div class="size-full">
|
||||
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
|
||||
<div class="h-full w-full">
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Semi-transparent overlay -->
|
||||
<div v-if="!isError" class="fixed inset-0 z-5 bg-neutral-900/80"></div>
|
||||
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
|
||||
|
||||
<!-- Smooth radial gradient overlay -->
|
||||
<div
|
||||
@@ -45,9 +45,9 @@
|
||||
<!-- Error Section (positioned at bottom) -->
|
||||
<div
|
||||
v-if="isError"
|
||||
class="absolute inset-x-0 bottom-20 flex flex-col items-center gap-4"
|
||||
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex justify-center gap-4">
|
||||
<div class="flex gap-4 justify-center">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
:label="$t('serverStart.reportIssue')"
|
||||
@@ -71,10 +71,10 @@
|
||||
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
|
||||
<div
|
||||
v-if="terminalVisible && isError"
|
||||
class="absolute inset-x-4 bottom-4 z-10 mx-auto max-w-4xl"
|
||||
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
|
||||
>
|
||||
<div
|
||||
class="h-[300px] rounded-lg border border-neutral-700 bg-neutral-900/95 p-4"
|
||||
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
|
||||
>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,7 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
|
||||
### Node.js & Playwright Prerequisites
|
||||
|
||||
Ensure you have the Node.js version from `.nvmrc` installed (currently v24).
|
||||
Then, set up the Chromium test driver:
|
||||
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
|
||||
|
||||
```bash
|
||||
pnpm exec playwright install chromium --with-deps
|
||||
|
||||
@@ -36,7 +36,14 @@
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple",
|
||||
"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"]
|
||||
},
|
||||
|
||||
@@ -206,7 +206,9 @@ export class ComfyPage {
|
||||
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
|
||||
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
|
||||
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
|
||||
this.runButton = page.getByTestId(TestIds.topbar.queueButton)
|
||||
this.runButton = page
|
||||
.getByTestId(TestIds.topbar.queueButton)
|
||||
.getByRole('button', { name: 'Run' })
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
@@ -430,10 +432,7 @@ export const comfyPageFixture = base.extend<{
|
||||
'Comfy.VueNodes.AutoScaleLayout': false,
|
||||
// Disable toast warning about version compatibility, as they may or
|
||||
// may not appear - depending on upstream ComfyUI dependencies
|
||||
'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
|
||||
'Comfy.VersionCompatibility.DisableWarnings': true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -172,19 +172,6 @@ export class VueNodeHelpers {
|
||||
async enterSubgraph(nodeId?: string): Promise<void> {
|
||||
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
|
||||
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
|
||||
|
||||
// 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 }
|
||||
})
|
||||
await editButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ export const TestIds = {
|
||||
},
|
||||
topbar: {
|
||||
queueButton: 'queue-button',
|
||||
queueModeMenuTrigger: 'queue-mode-menu-trigger',
|
||||
saveButton: 'save-workflow-button'
|
||||
},
|
||||
nodeLibrary: {
|
||||
|
||||
@@ -29,10 +29,8 @@ class ComfyQueueButton {
|
||||
public readonly dropdownButton: Locator
|
||||
constructor(public readonly actionbar: ComfyActionbar) {
|
||||
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
|
||||
this.primaryButton = this.root
|
||||
this.dropdownButton = actionbar.root.getByTestId(
|
||||
TestIds.topbar.queueModeMenuTrigger
|
||||
)
|
||||
this.primaryButton = this.root.locator('.p-splitbutton-button')
|
||||
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
|
||||
}
|
||||
|
||||
public async toggleOptions() {
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function getPromotedWidgetCount(
|
||||
return promotedWidgets.length
|
||||
}
|
||||
|
||||
export async function getPromotedWidgetCountByName(
|
||||
export function getPromotedWidgetCountByName(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: string,
|
||||
widgetName: string
|
||||
|
||||
@@ -89,17 +89,6 @@ test.describe('Execution error', () => {
|
||||
})
|
||||
|
||||
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 }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.ShowMissingModelsWarning',
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
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 |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 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: 88 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
@@ -22,10 +22,8 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.page
|
||||
.locator('[data-node-id]')
|
||||
.filter({ hasText: 'Load Checkpoint' })
|
||||
.getByTestId('node-inner-wrapper')
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
@@ -43,14 +41,8 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
|
||||
|
||||
const checkpointNode = comfyPage.page
|
||||
.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')
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 140 KiB |
@@ -3,7 +3,7 @@ import {
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const ERROR_CLASS = /ring-destructive-background/
|
||||
const ERROR_CLASS = /border-node-stroke-error/
|
||||
|
||||
test.describe('Vue Node Error', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -18,10 +18,9 @@ test.describe('Vue Node Error', () => {
|
||||
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
|
||||
|
||||
// Expect error state on missing unknown node
|
||||
const unknownNode = comfyPage.page
|
||||
.locator('[data-node-id]')
|
||||
.filter({ hasText: 'UNKNOWN NODE' })
|
||||
.getByTestId('node-inner-wrapper')
|
||||
const unknownNode = comfyPage.page.locator('[data-node-id]').filter({
|
||||
hasText: 'UNKNOWN NODE'
|
||||
})
|
||||
await expect(unknownNode).toHaveClass(ERROR_CLASS)
|
||||
})
|
||||
|
||||
@@ -32,10 +31,7 @@ test.describe('Vue Node Error', () => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
|
||||
await comfyPage.runButton.click()
|
||||
|
||||
const raiseErrorNode = comfyPage.page
|
||||
.locator('[data-node-id]')
|
||||
.filter({ hasText: 'Raise Error' })
|
||||
.getByTestId('node-inner-wrapper')
|
||||
const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error')
|
||||
await expect(raiseErrorNode).toHaveClass(ERROR_CLASS)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.42.2",
|
||||
"version": "1.41.13",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -89,13 +89,11 @@
|
||||
"chart.js": "^4.5.0",
|
||||
"cva": "catalog:",
|
||||
"dompurify": "^3.2.5",
|
||||
"dotenv": "catalog:",
|
||||
"es-toolkit": "^1.39.9",
|
||||
"extendable-media-recorder": "^9.2.27",
|
||||
"extendable-media-recorder-wav-encoder": "^7.0.129",
|
||||
"firebase": "catalog:",
|
||||
"fuse.js": "^7.0.0",
|
||||
"glob": "catalog:",
|
||||
"jsonata": "catalog:",
|
||||
"jsondiffpatch": "catalog:",
|
||||
"loglevel": "^1.9.2",
|
||||
@@ -145,6 +143,7 @@
|
||||
"@vue/test-utils": "catalog:",
|
||||
"@webgpu/types": "catalog:",
|
||||
"cross-env": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-prettier": "catalog:",
|
||||
"eslint-import-resolver-typescript": "catalog:",
|
||||
@@ -155,6 +154,7 @@
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "catalog:",
|
||||
"globals": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"husky": "catalog:",
|
||||
@@ -195,9 +195,6 @@
|
||||
"zip-dir": "^2.0.0",
|
||||
"zod-to-json-schema": "catalog:"
|
||||
},
|
||||
"engines": {
|
||||
"node": "24.x"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "catalog:"
|
||||
|
||||
71
pnpm-lock.yaml
generated
@@ -482,9 +482,6 @@ importers:
|
||||
dompurify:
|
||||
specifier: ^3.2.5
|
||||
version: 3.3.1
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
es-toolkit:
|
||||
specifier: ^1.39.9
|
||||
version: 1.39.10
|
||||
@@ -500,9 +497,6 @@ importers:
|
||||
fuse.js:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
glob:
|
||||
specifier: 'catalog:'
|
||||
version: 13.0.6
|
||||
jsonata:
|
||||
specifier: 'catalog:'
|
||||
version: 2.1.0
|
||||
@@ -645,6 +639,9 @@ importers:
|
||||
cross-env:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.0
|
||||
dotenv:
|
||||
specifier: 'catalog:'
|
||||
version: 16.6.1
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.39.1(jiti@2.6.1)
|
||||
@@ -675,6 +672,9 @@ importers:
|
||||
fs-extra:
|
||||
specifier: ^11.2.0
|
||||
version: 11.3.2
|
||||
glob:
|
||||
specifier: 'catalog:'
|
||||
version: 13.0.6
|
||||
globals:
|
||||
specifier: 'catalog:'
|
||||
version: 16.5.0
|
||||
@@ -2451,21 +2451,25 @@ packages:
|
||||
resolution: {integrity: sha512-D+tPXB0tkSuDPsuXvyQIsF3f3PBWfAwIe9FkBWtVoDVYqE+jbz+tVGsjQMNWGafLE4sC8ZQdjhsxyT8I53Anbw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-arm64-musl@22.5.2':
|
||||
resolution: {integrity: sha512-UbO527qqa8KLBi13uXto5SmxcZv1Smer7sPexJonshDlmrJsyvx5m8nm6tcSv04W5yQEL90vPlTux8dNvEDWrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-linux-x64-gnu@22.5.2':
|
||||
resolution: {integrity: sha512-wR6596Vr/Z+blUAmjLHG2TCQMs4O1oi9JXK1J/PoPeO9UqdHwStCJBAd61zDFSUYJe0x+dkeRQu96fE5BW8Kcg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@nx/nx-linux-x64-musl@22.5.2':
|
||||
resolution: {integrity: sha512-MBXOw4AH4FWl4orwVykj/e75awTNDePogrl3pXNX9NcQLdj6JzS4e2jaALQeRBQLxQzeFvFQV/W4PBzoPV6/NA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@nx/nx-win32-arm64-msvc@22.5.2':
|
||||
resolution: {integrity: sha512-SaWSZkRH5uV8vP2lj6RRv+kw2IzaIDXkutReOXpooshIWZl9KjrQELNTCZTYyhLDsMlcyhSvLFlTiA4NkZ8udw==}
|
||||
@@ -2631,41 +2635,49 @@ packages:
|
||||
resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-arm64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-ppc64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-riscv64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-linux-s390x-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-gnu@11.15.0':
|
||||
resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-resolver/binding-linux-x64-musl@11.15.0':
|
||||
resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-resolver/binding-openharmony-arm64@11.15.0':
|
||||
resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==}
|
||||
@@ -2739,48 +2751,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-arm64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-ppc64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-riscv64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-linux-s390x-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-gnu@0.34.0':
|
||||
resolution: {integrity: sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxfmt/binding-linux-x64-musl@0.34.0':
|
||||
resolution: {integrity: sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxfmt/binding-openharmony-arm64@0.34.0':
|
||||
resolution: {integrity: sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg==}
|
||||
@@ -2883,48 +2903,56 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-arm64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-ppc64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-riscv64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-linux-s390x-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-gnu@1.49.0':
|
||||
resolution: {integrity: sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxlint/binding-linux-x64-musl@1.49.0':
|
||||
resolution: {integrity: sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxlint/binding-openharmony-arm64@1.49.0':
|
||||
resolution: {integrity: sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA==}
|
||||
@@ -3093,24 +3121,28 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.0.0-rc.3':
|
||||
resolution: {integrity: sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ==}
|
||||
@@ -3188,56 +3220,67 @@ packages:
|
||||
resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.53.5':
|
||||
resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.53.5':
|
||||
resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.53.5':
|
||||
resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.53.5':
|
||||
resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==}
|
||||
@@ -3513,24 +3556,28 @@ packages:
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.2.0':
|
||||
resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.2.0':
|
||||
resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.2.0':
|
||||
resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==}
|
||||
@@ -4006,41 +4053,49 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -6416,24 +6471,28 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.31.1:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
lightningcss-linux-x64-musl@1.31.1:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.31.1:
|
||||
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
|
||||
|
||||
@@ -133,7 +133,7 @@ function isKeyUsed(key: string, sourceFiles: string[]): boolean {
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function checkNewUnusedKeys() {
|
||||
function checkNewUnusedKeys() {
|
||||
const stagedLocaleFiles = getStagedLocaleFiles()
|
||||
|
||||
if (stagedLocaleFiles.length === 0) {
|
||||
@@ -165,7 +165,7 @@ async function checkNewUnusedKeys() {
|
||||
if (unusedNewKeys.length > 0) {
|
||||
console.warn('\n⚠️ Warning: Found unused NEW i18n keys:\n')
|
||||
|
||||
for (const key of unusedNewKeys.sort()) {
|
||||
for (const key of unusedNewKeys.sort((a, b) => a.localeCompare(b))) {
|
||||
console.warn(` - ${key}`)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,9 @@ async function checkNewUnusedKeys() {
|
||||
}
|
||||
|
||||
// Run the check
|
||||
checkNewUnusedKeys().catch((err) => {
|
||||
try {
|
||||
checkNewUnusedKeys()
|
||||
} catch (err) {
|
||||
console.error('Error checking unused keys:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ function resolveRelease(
|
||||
return null
|
||||
}
|
||||
|
||||
const [major, currentMinor, patch] = currentVersion.split('.').map(Number)
|
||||
const [, currentMinor] = currentVersion.split('.').map(Number)
|
||||
|
||||
// Fetch all branches
|
||||
exec('git fetch origin', frontendRepoPath)
|
||||
@@ -264,7 +264,7 @@ if (!releaseInfo) {
|
||||
}
|
||||
|
||||
// Output as JSON for GitHub Actions
|
||||
|
||||
// oxlint-disable-next-line no-console -- CI script output
|
||||
console.log(JSON.stringify(releaseInfo, null, 2))
|
||||
|
||||
export { resolveRelease }
|
||||
|
||||
@@ -51,10 +51,10 @@ export function downloadFile(url: string, filename?: string): void {
|
||||
|
||||
/**
|
||||
* Download a Blob by creating a temporary object URL and anchor element
|
||||
* @param filename - The filename to suggest to the browser
|
||||
* @param blob - The Blob to download
|
||||
* @param filename - The filename to suggest to the browser
|
||||
*/
|
||||
export function downloadBlob(filename: string, blob: Blob): void {
|
||||
export function downloadBlob(blob: Blob, filename: string): void {
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
triggerLinkDownload(url, filename)
|
||||
@@ -138,7 +138,7 @@ async function downloadViaBlobFetch(
|
||||
extractFilenameFromContentDisposition(contentDisposition)
|
||||
|
||||
const blob = await response.blob()
|
||||
downloadBlob(headerFilename ?? fallbackFilename, blob)
|
||||
downloadBlob(blob, headerFilename ?? fallbackFilename)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,19 +45,19 @@ export const usdToCredits = (usd: number): number =>
|
||||
export const creditsToUsd = (credits: number): number =>
|
||||
Math.round((credits / CREDITS_PER_USD) * 100) / 100
|
||||
|
||||
export type FormatOptions = {
|
||||
type FormatOptions = {
|
||||
value: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
export type FormatFromCentsOptions = {
|
||||
type FormatFromCentsOptions = {
|
||||
cents: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
}
|
||||
|
||||
export type FormatFromUsdOptions = {
|
||||
type FormatFromUsdOptions = {
|
||||
usd: number
|
||||
locale?: string
|
||||
numberOptions?: Intl.NumberFormatOptions
|
||||
@@ -113,13 +113,3 @@ export const formatUsdFromCents = ({
|
||||
locale,
|
||||
numberOptions
|
||||
})
|
||||
|
||||
/**
|
||||
* Clamps a USD value to the allowed range for credit purchases
|
||||
* @param value - The USD amount to clamp
|
||||
* @returns The clamped value between $1 and $1000, or 0 if NaN
|
||||
*/
|
||||
export const clampUsd = (value: number): number => {
|
||||
if (Number.isNaN(value)) return 0
|
||||
return Math.min(1000, Math.max(1, value))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
/**
|
||||
* Utilities for pointer event handling
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if a pointer or mouse event is a middle button input
|
||||
* @param event - The pointer or mouse event to check
|
||||
* @returns true if the event is from the middle button/wheel
|
||||
*/
|
||||
export function isMiddlePointerInput(
|
||||
event: PointerEvent | MouseEvent
|
||||
): boolean {
|
||||
|
||||
@@ -166,22 +166,13 @@ describe('TopMenuSection', () => {
|
||||
})
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
mockData.isLoggedIn = true
|
||||
})
|
||||
|
||||
it('should display CurrentUserButton and not display LoginButton', () => {
|
||||
const wrapper = createLegacyTabBarWrapper()
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
|
||||
})
|
||||
@@ -195,7 +186,7 @@ describe('TopMenuSection', () => {
|
||||
describe('on desktop platform', () => {
|
||||
it('should display LoginButton and not display CurrentUserButton', () => {
|
||||
mockData.isDesktop = true
|
||||
const wrapper = createLegacyTabBarWrapper()
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findComponent(LoginButton).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
|
||||
})
|
||||
@@ -203,7 +194,7 @@ describe('TopMenuSection', () => {
|
||||
|
||||
describe('on web platform', () => {
|
||||
it('should not display CurrentUserButton and not display LoginButton', () => {
|
||||
const wrapper = createLegacyTabBarWrapper()
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
|
||||
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -141,7 +141,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
@@ -183,7 +183,7 @@ const isActionbarFloating = computed(
|
||||
() => isActionbarEnabled.value && !isActionbarDocked.value
|
||||
)
|
||||
const isIntegratedTabBar = computed(
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy'
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||
)
|
||||
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
|
||||
useQueueFeatureFlags()
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
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,129 +1,71 @@
|
||||
<template>
|
||||
<div
|
||||
v-tooltip.bottom="{
|
||||
value: t('menu.batchCount'),
|
||||
value: $t('menu.batchCount'),
|
||||
showDelay: 600
|
||||
}"
|
||||
class="batch-count h-full"
|
||||
:aria-label="t('menu.batchCount')"
|
||||
class="batch-count"
|
||||
:aria-label="$t('menu.batchCount')"
|
||||
>
|
||||
<div
|
||||
class="flex h-full w-14 overflow-hidden rounded-l-lg bg-secondary-background"
|
||||
>
|
||||
<input
|
||||
ref="batchCountInputRef"
|
||||
v-model="batchCountInput"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:aria-label="t('menu.batchCount')"
|
||||
:class="inputClass"
|
||||
@focus="onInputFocus"
|
||||
@input="onInput"
|
||||
@blur="onInputBlur"
|
||||
@keydown.enter.prevent="onInputEnter"
|
||||
/>
|
||||
<div class="flex h-full w-6 flex-col">
|
||||
<Button
|
||||
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>
|
||||
<InputNumber
|
||||
v-model="batchCount"
|
||||
class="w-14"
|
||||
:min="minQueueCount"
|
||||
:max="maxQueueCount"
|
||||
fluid
|
||||
show-buttons
|
||||
:pt="{
|
||||
incrementButton: {
|
||||
class: 'w-6',
|
||||
onmousedown: () => {
|
||||
handleClick(true)
|
||||
}
|
||||
},
|
||||
decrementButton: {
|
||||
class: 'w-6',
|
||||
onmousedown: () => {
|
||||
handleClick(false)
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TinyChevronIcon from './TinyChevronIcon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
const { batchCount } = storeToRefs(queueSettingsStore)
|
||||
const minQueueCount = 1
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const minQueueCount = 1
|
||||
const maxQueueCount = computed(() =>
|
||||
settingStore.get('Comfy.QueueButton.BatchCountLimit')
|
||||
)
|
||||
|
||||
const batchCountInputRef = ref<HTMLInputElement | null>(null)
|
||||
const batchCountInput = ref(String(batchCount.value))
|
||||
const isEditing = ref(false)
|
||||
|
||||
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 handleClick = (increment: boolean) => {
|
||||
let newCount: number
|
||||
if (increment) {
|
||||
const originalCount = batchCount.value - 1
|
||||
newCount = Math.min(originalCount * 2, maxQueueCount.value)
|
||||
} else {
|
||||
const originalCount = batchCount.value + 1
|
||||
newCount = Math.floor(originalCount / 2)
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
batchCount.value = newCount
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-inputtext) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
useDraggable,
|
||||
useEventListener,
|
||||
@@ -108,7 +108,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { buildTooltipConfig } from '@/utils/tooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
@@ -41,9 +41,28 @@ vi.mock('@/stores/workspaceStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
const BatchCountEditStub = {
|
||||
template: '<div data-testid="batch-count-edit" />'
|
||||
}
|
||||
const SplitButtonStub = defineComponent({
|
||||
name: 'SplitButton',
|
||||
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({
|
||||
legacy: false,
|
||||
@@ -88,26 +107,14 @@ function createWrapper() {
|
||||
tooltip: () => {}
|
||||
},
|
||||
stubs: {
|
||||
BatchCountEdit: BatchCountEditStub,
|
||||
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>' }
|
||||
SplitButton: SplitButtonStub,
|
||||
BatchCountEdit: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const wrapper = createWrapper()
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
@@ -117,10 +124,10 @@ describe('ComfyQueueButton', () => {
|
||||
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
|
||||
await nextTick()
|
||||
|
||||
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||
|
||||
expect(queueButton.text()).toContain('Run (Instant)')
|
||||
expect(queueButton.attributes('data-variant')).toBe('primary')
|
||||
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
||||
expect(splitButton.attributes('data-severity')).toBe('primary')
|
||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -131,10 +138,10 @@ describe('ComfyQueueButton', () => {
|
||||
queueSettingsStore.mode = 'instant-running'
|
||||
await nextTick()
|
||||
|
||||
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||
|
||||
expect(queueButton.text()).toContain('Stop Run (Instant)')
|
||||
expect(queueButton.attributes('data-variant')).toBe('destructive')
|
||||
expect(splitButton.attributes('data-label')).toBe('Stop Run (Instant)')
|
||||
expect(splitButton.attributes('data-severity')).toBe('danger')
|
||||
expect(wrapper.find('.icon-\\[lucide--square\\]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -152,17 +159,19 @@ describe('ComfyQueueButton', () => {
|
||||
await nextTick()
|
||||
|
||||
expect(queueSettingsStore.mode).toBe('instant-idle')
|
||||
const queueButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
|
||||
expect(queueButtonWhileStopping.text()).toContain('Run (Instant)')
|
||||
expect(queueButtonWhileStopping.attributes('data-variant')).toBe('primary')
|
||||
const splitButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
|
||||
expect(splitButtonWhileStopping.attributes('data-label')).toBe(
|
||||
'Run (Instant)'
|
||||
)
|
||||
expect(splitButtonWhileStopping.attributes('data-severity')).toBe('primary')
|
||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||
|
||||
expect(commandStore.execute).not.toHaveBeenCalled()
|
||||
|
||||
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||
expect(queueSettingsStore.mode).toBe('instant-idle')
|
||||
expect(queueButton.text()).toContain('Run (Instant)')
|
||||
expect(queueButton.attributes('data-variant')).toBe('primary')
|
||||
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
||||
expect(splitButton.attributes('data-severity')).toBe('primary')
|
||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,83 +1,48 @@
|
||||
<template>
|
||||
<ButtonGroup
|
||||
class="queue-button-group h-8 rounded-lg bg-secondary-background"
|
||||
>
|
||||
<BatchCountEdit />
|
||||
<Button
|
||||
<div class="queue-button-group flex">
|
||||
<SplitButton
|
||||
v-tooltip.bottom="{
|
||||
value: queueButtonTooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:variant="queueButtonVariant"
|
||||
size="unset"
|
||||
:class="queueActionButtonClass"
|
||||
class="comfyui-queue-button"
|
||||
:label="queueButtonLabel"
|
||||
:severity="queueButtonSeverity"
|
||||
size="small"
|
||||
:model="queueModeMenuItems"
|
||||
data-testid="queue-button"
|
||||
:data-variant="queueButtonVariant"
|
||||
@click="queuePrompt"
|
||||
>
|
||||
<i :class="cn(iconClass, 'size-4')" />
|
||||
{{ queueButtonLabel }}
|
||||
</Button>
|
||||
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<template #icon>
|
||||
<i :class="iconClass" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:class="queueMenuTriggerClass"
|
||||
:aria-label="t('menu.run')"
|
||||
data-testid="queue-mode-menu-trigger"
|
||||
v-tooltip="{
|
||||
value: item.tooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:variant="item.key === selectedQueueMode ? 'primary' : 'secondary'"
|
||||
size="sm"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<TinyChevronIcon />
|
||||
<i v-if="item.icon" :class="item.icon" />
|
||||
{{ String(item.label ?? '') }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
: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>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger
|
||||
} from 'reka-ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
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 ButtonGroup from '@/components/ui/button-group/ButtonGroup.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -89,9 +54,10 @@ import {
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
||||
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
@@ -103,60 +69,50 @@ const hasMissingNodes = computed(() =>
|
||||
const { t } = useI18n()
|
||||
type QueueModeMenuKey = 'disabled' | 'change' | 'instant-idle'
|
||||
|
||||
interface QueueModeMenuItem {
|
||||
key: QueueModeMenuKey
|
||||
label: string
|
||||
tooltip: string
|
||||
command: () => void
|
||||
}
|
||||
|
||||
const selectedQueueMode = computed<QueueModeMenuKey>(() =>
|
||||
isInstantMode(queueMode.value) ? 'instant-idle' : queueMode.value
|
||||
)
|
||||
|
||||
const queueModeMenuItemLookup = computed<Record<string, QueueModeMenuItem>>(
|
||||
() => {
|
||||
const items: Record<string, QueueModeMenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_on_change_selected'
|
||||
})
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
const items: Record<string, MenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_on_change_selected'
|
||||
})
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCloud) {
|
||||
items['instant-idle'] = {
|
||||
key: 'instant-idle',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_instant_selected'
|
||||
})
|
||||
queueMode.value = 'instant-idle'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
)
|
||||
if (!isCloud) {
|
||||
items['instant-idle'] = {
|
||||
key: 'instant-idle',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_instant_selected'
|
||||
})
|
||||
queueMode.value = 'instant-idle'
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const activeQueueModeMenuItem = computed(() => {
|
||||
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
||||
return (
|
||||
queueModeMenuItemLookup.value[selectedQueueMode.value] ||
|
||||
queueModeMenuItemLookup.value.disabled
|
||||
@@ -176,13 +132,9 @@ const queueButtonLabel = computed(() =>
|
||||
: String(activeQueueModeMenuItem.value?.label ?? '')
|
||||
)
|
||||
|
||||
const queueButtonVariant = computed<'destructive' | 'primary'>(() =>
|
||||
isStopInstantAction.value ? 'destructive' : 'primary'
|
||||
const queueButtonSeverity = computed(() =>
|
||||
isStopInstantAction.value ? 'danger' : '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(() => {
|
||||
if (isStopInstantAction.value) {
|
||||
@@ -249,3 +201,10 @@ const queuePrompt = async (e: Event) => {
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<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>
|
||||
@@ -7,6 +7,7 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import {
|
||||
openShareDialog,
|
||||
@@ -43,77 +44,91 @@ function openAssets() {
|
||||
function showApps() {
|
||||
void commandStore.execute('Workspace.ToggleSidebarTab.apps')
|
||||
}
|
||||
|
||||
function openTemplates() {
|
||||
useWorkflowTemplateSelectorDialog().show('sidebar')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pointer-events-auto flex flex-row items-start gap-2">
|
||||
<div class="pointer-events-auto flex flex-col gap-2">
|
||||
<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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pointer-events-auto flex flex-col gap-2">
|
||||
<WorkflowActionsDropdown source="app_mode_toolbar" />
|
||||
|
||||
<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
|
||||
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>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,7 @@ const mockCommands: ComfyCommandImpl[] = [
|
||||
icon: 'pi pi-test',
|
||||
tooltip: 'Test tooltip',
|
||||
menubarLabel: 'Other Command',
|
||||
keybinding: null
|
||||
keybinding: undefined
|
||||
} as ComfyCommandImpl
|
||||
]
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('ShortcutsList', () => {
|
||||
id: 'No.Keybinding',
|
||||
label: 'No Keybinding',
|
||||
category: 'essentials',
|
||||
keybinding: null
|
||||
keybinding: undefined
|
||||
} as ComfyCommandImpl
|
||||
]
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import PropertiesAccordionItem from '@/components/rightSidePanel/layout/Properti
|
||||
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
@@ -24,9 +25,9 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { DOMWidgetImpl } from '@/scripts/domWidget'
|
||||
import { promptRenameWidget } from '@/utils/widgetUtil'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
||||
@@ -72,12 +73,15 @@ const inputsWithState = computed(() =>
|
||||
}
|
||||
}
|
||||
|
||||
const input = node.inputs.find((i) => i.widget?.name === widget.name)
|
||||
const rename = input && (() => renameWidget(widget, input))
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
widgetName,
|
||||
label: widget.label,
|
||||
subLabel: node.title,
|
||||
rename: () => promptRenameWidget(widget, node, t)
|
||||
rename
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -88,6 +92,20 @@ const outputsWithState = computed<[NodeId, string][]>(() =>
|
||||
])
|
||||
)
|
||||
|
||||
async function renameWidget(widget: IBaseWidget, input: INodeInputSlot) {
|
||||
const newLabel = await useDialogService().prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('g.enterNewNamePrompt'),
|
||||
defaultValue: widget.label,
|
||||
placeholder: widget.name
|
||||
})
|
||||
if (newLabel === null) return
|
||||
widget.label = newLabel || undefined
|
||||
input.label = newLabel || undefined
|
||||
widget.callback?.(widget.value)
|
||||
useCanvasStore().canvas?.setDirty(true)
|
||||
}
|
||||
|
||||
function getHovered(
|
||||
e: MouseEvent
|
||||
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
|
||||
@@ -144,11 +162,7 @@ function handleDown(e: MouseEvent) {
|
||||
}
|
||||
function handleClick(e: MouseEvent) {
|
||||
const [node, widget] = getHovered(e) ?? []
|
||||
if (
|
||||
node?.mode !== LGraphEventMode.ALWAYS ||
|
||||
!nodeTypeValidForApp(node.type) ||
|
||||
node.has_errors
|
||||
)
|
||||
if (node?.mode !== LGraphEventMode.ALWAYS)
|
||||
return canvasInteractions.forwardEventToCanvas(e)
|
||||
|
||||
if (!widget) {
|
||||
@@ -184,9 +198,7 @@ const renderedOutputs = computed(() => {
|
||||
return canvas
|
||||
.graph!.nodes.filter(
|
||||
(n) =>
|
||||
n.constructor.nodeData?.output_node &&
|
||||
n.mode === LGraphEventMode.ALWAYS &&
|
||||
!n.has_errors
|
||||
n.constructor.nodeData?.output_node && n.mode === LGraphEventMode.ALWAYS
|
||||
)
|
||||
.map(nodeToDisplayTuple)
|
||||
})
|
||||
@@ -205,7 +217,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
|
||||
}}
|
||||
</div>
|
||||
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<DraggableList
|
||||
v-if="isArrangeMode"
|
||||
v-slot="{ dragClass }"
|
||||
@@ -242,21 +254,26 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
: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--info] bg-muted-foreground" />
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="p-4 text-muted-foreground"
|
||||
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"
|
||||
@@ -286,12 +303,6 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
/>
|
||||
</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>
|
||||
<PropertiesAccordionItem
|
||||
v-if="isSelectOutputsMode"
|
||||
:label="t('nodeHelpPage.outputs')"
|
||||
@@ -303,15 +314,19 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
{{ t('nodeHelpPage.outputs') }}
|
||||
<i class="icon-[lucide--info] bg-muted-foreground" />
|
||||
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div
|
||||
class="p-4 text-muted-foreground"
|
||||
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"
|
||||
@@ -334,15 +349,6 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
<Button variant="muted-textonly" size="lg" @click="$emit('viewApp')">
|
||||
{{ $t('builderToolbar.viewApp') }}
|
||||
</Button>
|
||||
<Button variant="secondary" size="lg" @click="$emit('exitToWorkflow')">
|
||||
{{ $t('builderToolbar.exitToWorkflow') }}
|
||||
<Button variant="secondary" size="lg" @click="$emit('close')">
|
||||
{{ $t('g.close') }}
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
@@ -58,6 +58,5 @@ defineProps<{
|
||||
defineEmits<{
|
||||
viewApp: []
|
||||
close: []
|
||||
exitToWorkflow: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -58,6 +58,6 @@ useEventListener(window, 'keydown', (e: KeyboardEvent) => {
|
||||
})
|
||||
|
||||
function onExitBuilder() {
|
||||
appModeStore.exitBuilder()
|
||||
void appModeStore.exitBuilder()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -72,7 +72,7 @@ const menuItems = computed(() => [
|
||||
},
|
||||
{
|
||||
label: t('builderMenu.exitAppBuilder'),
|
||||
icon: 'icon-[lucide--x]',
|
||||
icon: 'icon-[lucide--square-pen]',
|
||||
action: onExitBuilder
|
||||
}
|
||||
])
|
||||
@@ -94,7 +94,7 @@ function onEnterAppMode(close: () => void) {
|
||||
}
|
||||
|
||||
function onExitBuilder(close: () => void) {
|
||||
appModeStore.exitBuilder()
|
||||
void appModeStore.exitBuilder()
|
||||
close()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
@@ -7,13 +7,6 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
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<{
|
||||
title: string
|
||||
subTitle?: string
|
||||
@@ -39,28 +32,15 @@ const entries = computed(() => {
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="my-2 flex items-center-safe gap-2 rounded-lg p-2"
|
||||
data-testid="builder-io-item"
|
||||
>
|
||||
<div class="drag-handle mr-auto flex min-w-0 flex-col gap-1">
|
||||
<div
|
||||
v-tooltip.left="{ value: titleTooltip, showDelay: 300 }"
|
||||
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>
|
||||
<div class="my-2 flex items-center-safe gap-2 rounded-lg p-2">
|
||||
<div
|
||||
class="drag-handle mr-auto inline max-w-max min-w-0 flex-[4_1_0%] truncate"
|
||||
v-text="title"
|
||||
/>
|
||||
<div
|
||||
class="drag-handle inline max-w-max min-w-0 flex-[2_1_0%] truncate text-end text-muted-foreground"
|
||||
v-text="subTitle"
|
||||
/>
|
||||
<Popover :entries>
|
||||
<template #button>
|
||||
<Button variant="muted-textonly">
|
||||
|
||||
@@ -22,10 +22,6 @@ const mockApp = vi.hoisted(() => ({
|
||||
|
||||
const mockSetMode = vi.hoisted(() => vi.fn())
|
||||
|
||||
const mockAppModeStore = vi.hoisted(() => ({
|
||||
exitBuilder: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => mockDialogService
|
||||
}))
|
||||
@@ -46,10 +42,6 @@ vi.mock('@/composables/useAppMode', () => ({
|
||||
useAppMode: () => ({ setMode: mockSetMode })
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/appModeStore', () => ({
|
||||
useAppModeStore: () => mockAppModeStore
|
||||
}))
|
||||
|
||||
vi.mock('./DefaultViewDialogContent.vue', () => ({
|
||||
default: { name: 'MockDefaultViewDialogContent' }
|
||||
}))
|
||||
@@ -216,16 +208,6 @@ describe('useAppSetDefaultView', () => {
|
||||
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', () => {
|
||||
const confirmCall = applyAndGetConfirmDialog(true)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
import BuilderDefaultModeAppliedDialogContent from './BuilderDefaultModeAppliedDialogContent.vue'
|
||||
import DefaultViewDialogContent from './DefaultViewDialogContent.vue'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
|
||||
const DIALOG_KEY = 'builder-default-view'
|
||||
const APPLIED_DIALOG_KEY = 'builder-default-view-applied'
|
||||
@@ -17,7 +16,6 @@ export function useAppSetDefaultView() {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
const appModeStore = useAppModeStore()
|
||||
const { setMode } = useAppMode()
|
||||
|
||||
const settingView = computed(() => dialogStore.isDialogOpen(DIALOG_KEY))
|
||||
@@ -56,10 +54,6 @@ export function useAppSetDefaultView() {
|
||||
closeAppliedDialog()
|
||||
setMode('app')
|
||||
},
|
||||
onExitToWorkflow: () => {
|
||||
closeAppliedDialog()
|
||||
appModeStore.exitBuilder()
|
||||
},
|
||||
onClose: closeAppliedDialog
|
||||
}
|
||||
})
|
||||
|
||||