mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-23 22:07:32 +00:00
Compare commits
6 Commits
v1.41.5
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
160c615bc4 | ||
|
|
eb61c0bb4d | ||
|
|
4689581674 | ||
|
|
e4b456bb2c | ||
|
|
482ad401d4 | ||
|
|
9082f6bc3c |
10
.github/workflows/release-draft-create.yaml
vendored
10
.github/workflows/release-draft-create.yaml
vendored
@@ -53,7 +53,13 @@ jobs:
|
||||
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
|
||||
# Desktop-specific release artifact with desktop distribution flags.
|
||||
DISTRIBUTION=desktop pnpm build
|
||||
pnpm zipdist ./dist ./dist-desktop.zip
|
||||
|
||||
# Default release artifact for core/PyPI.
|
||||
NX_SKIP_NX_CACHE=true pnpm build
|
||||
pnpm zipdist
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -62,6 +68,7 @@ jobs:
|
||||
path: |
|
||||
dist/
|
||||
dist.zip
|
||||
dist-desktop.zip
|
||||
|
||||
draft_release:
|
||||
needs: build
|
||||
@@ -79,6 +86,7 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
dist.zip
|
||||
dist-desktop.zip
|
||||
tag_name: v${{ needs.build.outputs.version }}
|
||||
target_commitish: ${{ github.event.pull_request.base.ref }}
|
||||
make_latest: >-
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
"block-no-empty": true,
|
||||
"no-descending-specificity": null,
|
||||
"no-duplicate-at-import-rules": true,
|
||||
"at-rule-disallowed-list": ["apply"],
|
||||
"at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreAtRules": [
|
||||
"tailwind",
|
||||
"apply",
|
||||
"layer",
|
||||
"config",
|
||||
"theme",
|
||||
|
||||
@@ -61,8 +61,7 @@
|
||||
"^build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "vite build --config vite.config.mts"
|
||||
"command": "vite build --config apps/desktop-ui/vite.config.mts"
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/dist"
|
||||
|
||||
@@ -4,3 +4,39 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.p-button-secondary {
|
||||
border: none;
|
||||
background-color: var(--color-neutral-600);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.p-button-secondary:hover {
|
||||
background-color: var(--color-neutral-550);
|
||||
}
|
||||
|
||||
.p-button-secondary:active {
|
||||
background-color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
.p-button-danger {
|
||||
background-color: var(--color-coral-red-600);
|
||||
}
|
||||
|
||||
.p-button-danger:hover {
|
||||
background-color: var(--color-coral-red-500);
|
||||
}
|
||||
|
||||
.p-button-danger:active {
|
||||
background-color: var(--color-coral-red-400);
|
||||
}
|
||||
|
||||
.task-div .p-card {
|
||||
transition: opacity var(--default-transition-duration);
|
||||
--p-card-background: var(--p-button-secondary-background);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.task-div .p-card:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -101,13 +101,15 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
/* xterm renders its internal DOM outside Vue templates, so :deep selectors are
|
||||
* required to style those generated nodes.
|
||||
*/
|
||||
:deep(.p-terminal) .xterm {
|
||||
@apply overflow-hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
@apply bg-neutral-900 overflow-hidden;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-neutral-900);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -187,13 +187,17 @@ async function onLocaleChange(event: SelectChangeEvent) {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.p-dropdown-panel .p-dropdown-item) {
|
||||
@apply transition-colors;
|
||||
transition-property: color, background-color, border-color;
|
||||
transition-duration: var(--default-transition-duration);
|
||||
}
|
||||
|
||||
:deep(.p-dropdown) {
|
||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--color-neutral-900),
|
||||
0 0 0 4px color-mix(in srgb, var(--color-brand-yellow) 60%, transparent);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -269,26 +269,43 @@ const onFocus = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.location-picker-accordion) {
|
||||
@apply px-12;
|
||||
padding-inline: calc(var(--spacing) * 12);
|
||||
|
||||
.p-accordionpanel {
|
||||
@apply border-0 bg-transparent;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.p-accordionheader {
|
||||
@apply bg-neutral-800/50 border-0 rounded-xl mt-2 hover:bg-neutral-700/50;
|
||||
margin-top: calc(var(--spacing) * 2);
|
||||
border: 0;
|
||||
border-radius: var(--radius-xl);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-neutral-800) 50%,
|
||||
transparent
|
||||
);
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-radius 0.5s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-neutral-700) 50%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* When panel is expanded, adjust header border radius */
|
||||
.p-accordionpanel-active {
|
||||
.p-accordionheader {
|
||||
@apply rounded-t-xl rounded-b-none;
|
||||
border-top-left-radius: var(--radius-xl);
|
||||
border-top-right-radius: var(--radius-xl);
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.p-accordionheader-toggle-icon {
|
||||
@@ -299,11 +316,24 @@ const onFocus = async () => {
|
||||
}
|
||||
|
||||
.p-accordioncontent {
|
||||
@apply bg-neutral-800/50 border-0 rounded-b-xl rounded-t-none;
|
||||
border: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: var(--radius-xl);
|
||||
border-bottom-left-radius: var(--radius-xl);
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-neutral-800) 50%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.p-accordioncontent-content {
|
||||
@apply bg-transparent pt-3 pr-5 pb-5 pl-5;
|
||||
background-color: transparent;
|
||||
padding-top: calc(var(--spacing) * 3);
|
||||
padding-right: calc(var(--spacing) * 5);
|
||||
padding-bottom: calc(var(--spacing) * 5);
|
||||
padding-left: calc(var(--spacing) * 5);
|
||||
}
|
||||
|
||||
/* Override default chevron icons to use up/down */
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div relative grid min-h-52 max-w-48"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
:class="
|
||||
cn(
|
||||
'task-div group/task-card relative grid min-h-52 max-w-48',
|
||||
isLoading && 'opacity-75'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Card
|
||||
class="relative h-full max-w-48 overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
:class="
|
||||
cn(
|
||||
'relative h-full max-w-48 overflow-hidden',
|
||||
runner.state !== 'error' && 'opacity-65'
|
||||
)
|
||||
"
|
||||
:pt="cardPt"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
@@ -43,7 +52,7 @@
|
||||
|
||||
<i
|
||||
v-if="!isLoading && runner.state === 'OK'"
|
||||
class="task-card-ok pi pi-check"
|
||||
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>
|
||||
@@ -55,6 +64,7 @@ import { computed } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
@@ -83,51 +93,9 @@ const reactiveExecuting = computed(() => !!runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
|
||||
const cardPt = {
|
||||
header: { class: 'z-0' },
|
||||
body: { class: 'z-[1] grow justify-between' }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.task-card-ok {
|
||||
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
|
||||
|
||||
font-size: 4rem;
|
||||
text-shadow: 0.25rem 0 0.5rem black;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.p-card {
|
||||
@apply transition-opacity;
|
||||
|
||||
--p-card-background: var(--p-button-secondary-background);
|
||||
opacity: 0.9;
|
||||
|
||||
&.opacity-65 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-card-header) {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
:deep(.p-card-body) {
|
||||
z-index: 1;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-div {
|
||||
> i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover > i {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<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) }}
|
||||
{{ $t(`desktopDialogs.${id}.title`, title) }}
|
||||
</h1>
|
||||
<p class="whitespace-pre-wrap">
|
||||
{{ t(`desktopDialogs.${id}.message`, message) }}
|
||||
{{ $t(`desktopDialogs.${id}.message`, message) }}
|
||||
</p>
|
||||
<div class="flex w-full gap-2">
|
||||
<Button
|
||||
@@ -12,7 +12,7 @@
|
||||
:key="button.label"
|
||||
class="rounded-lg first:mr-auto"
|
||||
:label="
|
||||
t(
|
||||
$t(
|
||||
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
|
||||
button.label
|
||||
)
|
||||
@@ -31,7 +31,6 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { getDialog } from '@/constants/desktopDialogs'
|
||||
import type { DialogAction } from '@/constants/desktopDialogs'
|
||||
import { t } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -41,31 +40,3 @@ const handleButtonClick = async (button: DialogAction) => {
|
||||
await electronAPI().Dialog.clickButton(button.returnValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.p-button-secondary {
|
||||
@apply text-white border-none bg-neutral-600;
|
||||
}
|
||||
|
||||
.p-button-secondary:hover {
|
||||
@apply bg-neutral-550;
|
||||
}
|
||||
|
||||
.p-button-secondary:active {
|
||||
@apply bg-neutral-500;
|
||||
}
|
||||
|
||||
.p-button-danger {
|
||||
@apply bg-coral-red-600;
|
||||
}
|
||||
|
||||
.p-button-danger:hover {
|
||||
@apply bg-coral-red-500;
|
||||
}
|
||||
|
||||
.p-button-danger:active {
|
||||
@apply bg-coral-red-400;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
<div class="relative m-8 text-center">
|
||||
<!-- Header -->
|
||||
<h1 class="download-bg pi-download text-4xl font-bold">
|
||||
{{ t('desktopUpdate.title') }}
|
||||
{{ $t('desktopUpdate.title') }}
|
||||
</h1>
|
||||
|
||||
<div class="m-8">
|
||||
<span>{{ t('desktopUpdate.description') }}</span>
|
||||
<span>{{ $t('desktopUpdate.description') }}</span>
|
||||
</div>
|
||||
|
||||
<ProgressSpinner class="m-8 w-48 h-48" />
|
||||
@@ -19,7 +19,7 @@
|
||||
<Button
|
||||
style="transform: translateX(-50%)"
|
||||
class="fixed bottom-0 left-1/2 my-8"
|
||||
:label="t('maintenance.consoleLogs')"
|
||||
:label="$t('maintenance.consoleLogs')"
|
||||
icon="pi pi-desktop"
|
||||
icon-pos="left"
|
||||
severity="secondary"
|
||||
@@ -28,8 +28,8 @@
|
||||
|
||||
<TerminalOutputDrawer
|
||||
v-model="terminalVisible"
|
||||
:header="t('g.terminal')"
|
||||
:default-message="t('desktopUpdate.terminalDefaultMessage')"
|
||||
:header="$t('g.terminal')"
|
||||
:default-message="$t('desktopUpdate.terminalDefaultMessage')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,7 +44,6 @@ import Toast from 'primevue/toast'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
||||
@@ -61,10 +60,10 @@ onUnmounted(() => electron.Validation.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.download-bg::before {
|
||||
@apply m-0 absolute text-muted;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-family: 'primeicons', sans-serif;
|
||||
top: -2rem;
|
||||
right: 2rem;
|
||||
|
||||
@@ -183,33 +183,37 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
:deep(.p-steppanel) {
|
||||
@apply mt-8 flex justify-center bg-transparent;
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Remove default padding/margin from StepPanels to make scrollbar flush */
|
||||
:deep(.p-steppanels) {
|
||||
@apply p-0 m-0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure StepPanel content container has no top/bottom padding */
|
||||
:deep(.p-steppanel-content) {
|
||||
@apply p-0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
|
||||
:deep(.p-steppanels::-webkit-scrollbar) {
|
||||
@apply w-4;
|
||||
width: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
:deep(.p-steppanels::-webkit-scrollbar-track) {
|
||||
@apply bg-transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
|
||||
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
|
||||
border: 4px solid transparent;
|
||||
border-radius: var(--radius-lg);
|
||||
background-color: color-mix(in srgb, var(--color-white) 20%, transparent);
|
||||
background-clip: content-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,12 +114,12 @@ import Tag from 'primevue/tag'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||
import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
|
||||
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
@@ -129,6 +129,7 @@ import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
||||
|
||||
const electron = electronAPI()
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const { clearResolved, processUpdate, refreshDesktopTasks } = taskStore
|
||||
|
||||
@@ -220,14 +221,14 @@ onUnmounted(() => electron.Validation.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
:deep(.p-tag) {
|
||||
--p-tag-gap: 0.375rem;
|
||||
}
|
||||
|
||||
.backspan::before {
|
||||
@apply m-0 absolute text-muted;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-family: 'primeicons', sans-serif;
|
||||
top: -2rem;
|
||||
right: -2rem;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseViewTemplate>
|
||||
<div class="sad-container">
|
||||
<div class="sad-container grid items-center justify-evenly">
|
||||
<!-- Right side image -->
|
||||
<img
|
||||
class="sad-girl"
|
||||
@@ -79,10 +79,7 @@ const continueToInstall = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
.sad-container {
|
||||
@apply grid items-center justify-evenly;
|
||||
grid-template-columns: 25rem 1fr;
|
||||
|
||||
& > * {
|
||||
|
||||
@@ -232,8 +232,6 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../assets/css/style.css';
|
||||
|
||||
/* Hide the xterm scrollbar completely */
|
||||
:deep(.p-terminal) .xterm-viewport {
|
||||
overflow: hidden !important;
|
||||
|
||||
@@ -44,6 +44,12 @@ export const TestIds = {
|
||||
node: {
|
||||
titleInput: 'node-title-input'
|
||||
},
|
||||
selectionToolbox: {
|
||||
colorPickerButton: 'color-picker-button',
|
||||
colorPickerCurrentColor: 'color-picker-current-color',
|
||||
colorBlue: 'blue',
|
||||
colorRed: 'red'
|
||||
},
|
||||
widgets: {
|
||||
decrement: 'decrement',
|
||||
increment: 'increment',
|
||||
@@ -74,6 +80,7 @@ export type TestIdValue =
|
||||
| (typeof TestIds.nodeLibrary)[keyof typeof TestIds.nodeLibrary]
|
||||
| (typeof TestIds.propertiesPanel)[keyof typeof TestIds.propertiesPanel]
|
||||
| (typeof TestIds.node)[keyof typeof TestIds.node]
|
||||
| (typeof TestIds.selectionToolbox)[keyof typeof TestIds.selectionToolbox]
|
||||
| (typeof TestIds.widgets)[keyof typeof TestIds.widgets]
|
||||
| (typeof TestIds.breadcrumb)[keyof typeof TestIds.breadcrumb]
|
||||
| Exclude<
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { TestIds } from '../fixtures/selectors'
|
||||
|
||||
const test = comfyPageFixture
|
||||
|
||||
@@ -10,6 +12,17 @@ test.beforeEach(async ({ comfyPage }) => {
|
||||
const BLUE_COLOR = 'rgb(51, 51, 85)'
|
||||
const RED_COLOR = 'rgb(85, 51, 51)'
|
||||
|
||||
const getColorPickerButton = (comfyPage: { page: Page }) =>
|
||||
comfyPage.page.getByTestId(TestIds.selectionToolbox.colorPickerButton)
|
||||
|
||||
const getColorPickerCurrentColor = (comfyPage: { page: Page }) =>
|
||||
comfyPage.page.getByTestId(TestIds.selectionToolbox.colorPickerCurrentColor)
|
||||
|
||||
const getColorPickerGroup = (comfyPage: { page: Page }) =>
|
||||
comfyPage.page.getByRole('group').filter({
|
||||
has: comfyPage.page.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
})
|
||||
|
||||
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
@@ -132,28 +145,24 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
|
||||
// Color picker button should be visible
|
||||
const colorPickerButton = comfyPage.page.locator(
|
||||
'.selection-toolbox .pi-circle-fill'
|
||||
)
|
||||
const colorPickerButton = getColorPickerButton(comfyPage)
|
||||
await expect(colorPickerButton).toBeVisible()
|
||||
|
||||
// Click color picker button
|
||||
await colorPickerButton.click()
|
||||
|
||||
// Color picker dropdown should be visible
|
||||
const colorPickerDropdown = comfyPage.page.locator(
|
||||
'.color-picker-container'
|
||||
)
|
||||
await expect(colorPickerDropdown).toBeVisible()
|
||||
const colorPickerGroup = getColorPickerGroup(comfyPage)
|
||||
await expect(colorPickerGroup).toBeVisible()
|
||||
|
||||
// Select a color (e.g., blue)
|
||||
const blueColorOption = colorPickerDropdown.locator(
|
||||
'i[data-testid="blue"]'
|
||||
const blueColorOption = colorPickerGroup.getByTestId(
|
||||
TestIds.selectionToolbox.colorBlue
|
||||
)
|
||||
await blueColorOption.click()
|
||||
|
||||
// Dropdown should close after selection
|
||||
await expect(colorPickerDropdown).not.toBeVisible()
|
||||
await expect(colorPickerGroup).not.toBeVisible()
|
||||
|
||||
// Node should have the selected color class/style
|
||||
// Note: Exact verification method depends on how color is applied to nodes
|
||||
@@ -172,22 +181,21 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
'CLIP Text Encode (Prompt)'
|
||||
])
|
||||
|
||||
const colorPickerButton = comfyPage.page.locator(
|
||||
'.selection-toolbox .pi-circle-fill'
|
||||
)
|
||||
const colorPickerButton = getColorPickerButton(comfyPage)
|
||||
const colorPickerCurrentColor = getColorPickerCurrentColor(comfyPage)
|
||||
|
||||
// Initially should show default color
|
||||
await expect(colorPickerButton).not.toHaveAttribute('color')
|
||||
|
||||
// Click color picker and select a color
|
||||
await colorPickerButton.click()
|
||||
const redColorOption = comfyPage.page.locator(
|
||||
'.color-picker-container i[data-testid="red"]'
|
||||
const redColorOption = getColorPickerGroup(comfyPage).getByTestId(
|
||||
TestIds.selectionToolbox.colorRed
|
||||
)
|
||||
await redColorOption.click()
|
||||
|
||||
// Button should now show the selected color
|
||||
await expect(colorPickerButton).toHaveCSS('color', RED_COLOR)
|
||||
await expect(colorPickerCurrentColor).toHaveCSS('color', RED_COLOR)
|
||||
})
|
||||
|
||||
test('color picker shows mixed state for differently colored selections', async ({
|
||||
@@ -195,17 +203,17 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
}) => {
|
||||
// Select first node and color it
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container i[data-testid="blue"]')
|
||||
await getColorPickerButton(comfyPage).click()
|
||||
await getColorPickerGroup(comfyPage)
|
||||
.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
.click()
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
|
||||
// Select second node and color it differently
|
||||
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container i[data-testid="red"]')
|
||||
await getColorPickerButton(comfyPage).click()
|
||||
await getColorPickerGroup(comfyPage)
|
||||
.getByTestId(TestIds.selectionToolbox.colorRed)
|
||||
.click()
|
||||
|
||||
// Select both nodes
|
||||
@@ -215,9 +223,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
])
|
||||
|
||||
// Color picker should show null/mixed state
|
||||
const colorPickerButton = comfyPage.page.locator(
|
||||
'.selection-toolbox .pi-circle-fill'
|
||||
)
|
||||
const colorPickerButton = getColorPickerButton(comfyPage)
|
||||
await expect(colorPickerButton).not.toHaveAttribute('color')
|
||||
})
|
||||
|
||||
@@ -226,9 +232,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
}) => {
|
||||
// First color a node
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container i[data-testid="blue"]')
|
||||
await getColorPickerButton(comfyPage).click()
|
||||
await getColorPickerGroup(comfyPage)
|
||||
.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
.click()
|
||||
|
||||
// Clear selection
|
||||
@@ -238,10 +244,8 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
|
||||
// Color picker button should show the correct color
|
||||
const colorPickerButton = comfyPage.page.locator(
|
||||
'.selection-toolbox .pi-circle-fill'
|
||||
)
|
||||
await expect(colorPickerButton).toHaveCSS('color', BLUE_COLOR)
|
||||
const colorPickerCurrentColor = getColorPickerCurrentColor(comfyPage)
|
||||
await expect(colorPickerCurrentColor).toHaveCSS('color', BLUE_COLOR)
|
||||
})
|
||||
|
||||
test('colorization via color picker can be undone', async ({
|
||||
@@ -249,9 +253,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
|
||||
}) => {
|
||||
// Select a node and color it
|
||||
await comfyPage.nodeOps.selectNodes(['KSampler'])
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container i[data-testid="blue"]')
|
||||
await getColorPickerButton(comfyPage).click()
|
||||
await getColorPickerGroup(comfyPage)
|
||||
.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
.click()
|
||||
|
||||
// Undo the colorization
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
import { TestIds } from '../../../fixtures/selectors'
|
||||
|
||||
test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -19,10 +20,16 @@ test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => {
|
||||
})
|
||||
await loadCheckpointNode.getByText('Load Checkpoint').click()
|
||||
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container')
|
||||
.locator('i[data-testid="blue"]')
|
||||
const colorPickerButton = comfyPage.page.getByTestId(
|
||||
TestIds.selectionToolbox.colorPickerButton
|
||||
)
|
||||
await colorPickerButton.click()
|
||||
|
||||
const colorPickerGroup = comfyPage.page.getByRole('group').filter({
|
||||
has: comfyPage.page.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
})
|
||||
await colorPickerGroup
|
||||
.getByTestId(TestIds.selectionToolbox.colorBlue)
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
@@ -39,6 +39,10 @@ Prefer Vue native options when available:
|
||||
- Use inline Tailwind CSS only (no `<style>` blocks)
|
||||
- Use `cn()` from `@/utils/tailwindUtil` for conditional classes
|
||||
- Refer to packages/design-system/src/css/style.css for design tokens and tailwind configuration
|
||||
- Exception: when third-party libraries render runtime DOM outside Vue templates
|
||||
(for example xterm internals inside PrimeVue terminal wrappers), scoped
|
||||
`:deep()` selectors are allowed. Add a brief inline comment explaining why the
|
||||
exception is required.
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ export default {
|
||||
'tests-ui/**': () =>
|
||||
'echo "Files in tests-ui/ are deprecated. Colocate tests with source files." && exit 1',
|
||||
|
||||
'./**/*.{css,vue}': (stagedFiles: string[]) => {
|
||||
const joinedPaths = toJoinedRelativePaths(stagedFiles)
|
||||
return [`pnpm exec stylelint --allow-empty-input ${joinedPaths}`]
|
||||
},
|
||||
|
||||
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => {
|
||||
@@ -22,12 +27,17 @@ export default {
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames: string[]) {
|
||||
// Convert absolute paths to relative paths for better ESLint resolution
|
||||
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
|
||||
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
const joinedPaths = toJoinedRelativePaths(fileNames)
|
||||
return [
|
||||
`pnpm exec oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
}
|
||||
|
||||
function toJoinedRelativePaths(fileNames: string[]) {
|
||||
const relativePaths = fileNames.map((f) =>
|
||||
path.relative(process.cwd(), f).replace(/\\/g, '/')
|
||||
)
|
||||
return relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@
|
||||
"knip": "knip --cache",
|
||||
"lint:fix:no-cache": "oxlint src --type-aware --fix && eslint src --fix",
|
||||
"lint:fix": "oxlint src --type-aware --fix && eslint src --cache --fix",
|
||||
"lint:no-cache": "oxlint src --type-aware && eslint src",
|
||||
"lint:no-cache": "pnpm exec stylelint '{apps,packages,src}/**/*.{css,vue}' && oxlint src --type-aware && eslint src",
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint": "oxlint src --type-aware && eslint src --cache",
|
||||
"lint": "pnpm stylelint && oxlint src --type-aware && eslint src --cache",
|
||||
"lint:desktop": "nx run @comfyorg/desktop-ui:lint",
|
||||
"locale": "lobe-i18n locale",
|
||||
"oxlint": "oxlint src --type-aware",
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
:root {
|
||||
--fg-color: #000;
|
||||
--bg-color: #fff;
|
||||
--default-transition-duration: 0.1s;
|
||||
--comfy-menu-bg: #353535;
|
||||
--comfy-menu-secondary-bg: #292929;
|
||||
--comfy-topbar-height: 2.5rem;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import zipdir from 'zip-dir'
|
||||
|
||||
zipdir('./dist', { saveTo: './dist.zip' }, function (err, buffer) {
|
||||
const sourceDir = process.argv[2] || './dist'
|
||||
const outputPath = process.argv[3] || './dist.zip'
|
||||
|
||||
zipdir(sourceDir, { saveTo: outputPath }, function (err, buffer) {
|
||||
if (err) {
|
||||
console.error('Error zipping "dist" directory:', err)
|
||||
console.error(`Error zipping "${sourceDir}" directory:`, err)
|
||||
} else {
|
||||
console.log('Successfully zipped "dist" directory.')
|
||||
process.stdout.write(
|
||||
`Successfully zipped "${sourceDir}" directory to "${outputPath}".\n`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -103,13 +103,12 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
:deep(.p-terminal) .xterm {
|
||||
@apply overflow-hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
@apply bg-neutral-900 overflow-hidden;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-neutral-900);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -195,8 +195,6 @@ onUpdated(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.subgraph-breadcrumb:not(:empty) {
|
||||
flex: auto;
|
||||
flex-shrink: 10000;
|
||||
@@ -205,7 +203,7 @@ onUpdated(() => {
|
||||
|
||||
.subgraph-breadcrumb,
|
||||
:deep(.p-breadcrumb) {
|
||||
@apply overflow-hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb) {
|
||||
@@ -214,7 +212,10 @@ onUpdated(() => {
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item) {
|
||||
@apply flex items-center overflow-hidden h-8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: calc(var(--spacing) * 8);
|
||||
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);
|
||||
border: 1px solid transparent;
|
||||
background-color: transparent;
|
||||
@@ -236,7 +237,7 @@ onUpdated(() => {
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:hover) {
|
||||
@apply rounded-lg;
|
||||
border-radius: var(--radius-lg);
|
||||
border-color: var(--interface-stroke);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
@@ -270,18 +271,16 @@ onUpdated(() => {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.subgraph-breadcrumb-collapse .p-breadcrumb-list {
|
||||
.p-breadcrumb-item,
|
||||
.p-breadcrumb-separator {
|
||||
@apply hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.p-breadcrumb-item:nth-last-child(3),
|
||||
.p-breadcrumb-separator:nth-last-child(2),
|
||||
.p-breadcrumb-item:nth-last-child(1) {
|
||||
@apply flex;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -186,19 +186,19 @@ const inputBlur = async (doRename: boolean) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.p-breadcrumb-item-link,
|
||||
.p-breadcrumb-item-icon {
|
||||
@apply select-none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.p-breadcrumb-item-link {
|
||||
@apply overflow-hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.p-breadcrumb-item-label {
|
||||
@apply whitespace-nowrap text-ellipsis overflow-hidden;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active-breadcrumb-item {
|
||||
|
||||
@@ -117,20 +117,18 @@ function getFormComponent(item: FormItem): Component {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.form-input :deep(.input-slider) .p-inputnumber input,
|
||||
.form-input :deep(.input-slider) .slider-part {
|
||||
@apply w-20;
|
||||
width: 5rem;
|
||||
}
|
||||
|
||||
.form-input :deep(.input-knob) .p-inputnumber input,
|
||||
.form-input :deep(.input-knob) .knob-part {
|
||||
@apply w-32;
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.form-input :deep(.p-inputtext),
|
||||
.form-input :deep(.p-select) {
|
||||
@apply w-44;
|
||||
width: 11rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -133,8 +133,6 @@ const wrapperStyle = computed(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.p-inputtext) {
|
||||
--p-form-field-padding-x: 0.625rem;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Chip removable @remove="$emit('remove', $event)">
|
||||
<Badge size="small" :class="badgeClass">
|
||||
<Chip removable @remove="emit('remove', $event)">
|
||||
<Badge size="small" :class="semanticBadgeClass">
|
||||
{{ badge }}
|
||||
</Badge>
|
||||
{{ text }}
|
||||
@@ -10,6 +10,7 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from 'primevue/badge'
|
||||
import Chip from 'primevue/chip'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export interface SearchFilter {
|
||||
text: string
|
||||
@@ -18,26 +19,19 @@ export interface SearchFilter {
|
||||
id: string | number
|
||||
}
|
||||
|
||||
defineProps<Omit<SearchFilter, 'id'>>()
|
||||
defineEmits(['remove'])
|
||||
const semanticClassMap: Record<string, string> = {
|
||||
'i-badge': 'bg-green-500 text-white',
|
||||
'o-badge': 'bg-red-500 text-white',
|
||||
'c-badge': 'bg-blue-500 text-white',
|
||||
's-badge': 'bg-yellow-500'
|
||||
}
|
||||
|
||||
const props = defineProps<Omit<SearchFilter, 'id'>>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove', event: Event): void
|
||||
}>()
|
||||
|
||||
const semanticBadgeClass = computed(() => {
|
||||
return semanticClassMap[props.badgeClass] ?? props.badgeClass
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.i-badge) {
|
||||
@apply bg-green-500 text-white;
|
||||
}
|
||||
|
||||
:deep(.o-badge) {
|
||||
@apply bg-red-500 text-white;
|
||||
}
|
||||
|
||||
:deep(.c-badge) {
|
||||
@apply bg-blue-500 text-white;
|
||||
}
|
||||
|
||||
:deep(.s-badge) {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,20 +71,30 @@ function getDialogPt(item: {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.global-dialog {
|
||||
max-width: calc(100vw - 1rem);
|
||||
}
|
||||
|
||||
.global-dialog .p-dialog-header {
|
||||
@apply p-2 2xl:p-[var(--p-dialog-header-padding)];
|
||||
@apply pb-0;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.global-dialog .p-dialog-content {
|
||||
@apply p-2 2xl:p-[var(--p-dialog-content-padding)];
|
||||
@apply pt-0;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.global-dialog .p-dialog-header {
|
||||
padding: var(--p-dialog-header-padding);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.global-dialog .p-dialog-content {
|
||||
padding: var(--p-dialog-content-padding);
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Workspace mode: wider settings dialog */
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
}"
|
||||
@row-dblclick="editKeybinding($event.data)"
|
||||
>
|
||||
<Column field="actions" header="">
|
||||
<Column field="actions" header="" :pt="{ bodyCell: 'p-1 min-h-8' }">
|
||||
<template #body="slotProps">
|
||||
<div class="actions invisible flex flex-row">
|
||||
<div class="actions flex flex-row">
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
@@ -56,6 +56,7 @@
|
||||
:header="$t('g.command')"
|
||||
sortable
|
||||
class="max-w-64 2xl:max-w-full"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="truncate" :title="slotProps.data.id">
|
||||
@@ -63,7 +64,11 @@
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="keybinding" :header="$t('g.keybinding')">
|
||||
<Column
|
||||
field="keybinding"
|
||||
:header="$t('g.keybinding')"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<KeyComboDisplay
|
||||
v-if="slotProps.data.keybinding"
|
||||
@@ -75,7 +80,11 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="source" :header="$t('g.source')">
|
||||
<Column
|
||||
field="source"
|
||||
:header="$t('g.source')"
|
||||
:pt="{ bodyCell: 'p-1 min-h-8' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<span class="overflow-hidden text-ellipsis">{{
|
||||
slotProps.data.source || '-'
|
||||
@@ -293,17 +302,3 @@ async function resetAllKeybindings() {
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
:deep(.p-datatable-tbody) > tr > td {
|
||||
@apply p-1;
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-row-selected) .actions,
|
||||
:deep(.p-datatable-selectable-row:hover) .actions {
|
||||
@apply visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,16 +98,17 @@ describe('SignInForm', () => {
|
||||
await nextTick()
|
||||
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
'span.text-muted.text-base.font-medium.select-none'
|
||||
)
|
||||
|
||||
expect(forgotPasswordSpan.classes()).toContain('text-link-disabled')
|
||||
expect(forgotPasswordSpan.classes()).toContain('cursor-not-allowed')
|
||||
expect(forgotPasswordSpan.classes()).toContain('opacity-50')
|
||||
})
|
||||
|
||||
it('shows toast and focuses email input when clicked while disabled', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
'span.text-muted.text-base.font-medium.select-none'
|
||||
)
|
||||
|
||||
// Mock getElementById to track focus
|
||||
@@ -152,7 +153,7 @@ describe('SignInForm', () => {
|
||||
)
|
||||
|
||||
const forgotPasswordSpan = wrapper.find(
|
||||
'span.text-muted.text-base.font-medium.cursor-pointer'
|
||||
'span.text-muted.text-base.font-medium.select-none'
|
||||
)
|
||||
|
||||
// Click the forgot password link
|
||||
|
||||
@@ -34,10 +34,13 @@
|
||||
{{ t('auth.login.passwordLabel') }}
|
||||
</label>
|
||||
<span
|
||||
class="cursor-pointer text-base font-medium text-muted select-none"
|
||||
:class="{
|
||||
'text-link-disabled': !$form.email?.value || $form.email?.invalid
|
||||
}"
|
||||
:class="
|
||||
cn('text-base font-medium text-muted select-none', {
|
||||
'cursor-not-allowed opacity-50':
|
||||
!$form.email?.value || $form.email?.invalid,
|
||||
'cursor-pointer': $form.email?.value && !$form.email?.invalid
|
||||
})
|
||||
"
|
||||
@click="handleForgotPassword($form.email?.value, $form.email?.valid)"
|
||||
>
|
||||
{{ t('auth.login.forgotPassword') }}
|
||||
@@ -89,6 +92,7 @@ import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthAction
|
||||
import { signInSchema } from '@/schemas/signInSchema'
|
||||
import type { SignInData } from '@/schemas/signInSchema'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const firebaseAuthActions = useFirebaseAuthActions()
|
||||
@@ -126,11 +130,3 @@ const handleForgotPassword = async (
|
||||
await firebaseAuthActions.sendPasswordReset(email)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
.text-link-disabled {
|
||||
@apply opacity-50 cursor-not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,20 +8,38 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
// Import after mocks
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
useWorkflowStore
|
||||
} from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { defaultGraph } from '@/scripts/defaultGraph'
|
||||
import { createMockPositionable } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
function createMockWorkflow(
|
||||
overrides: Partial<LoadedComfyWorkflow> = {}
|
||||
): LoadedComfyWorkflow {
|
||||
return {
|
||||
changeTracker: {
|
||||
const workflow = new ComfyWorkflow({
|
||||
path: 'workflows/color-picker-test.json',
|
||||
modified: 0,
|
||||
size: 0
|
||||
})
|
||||
|
||||
const changeTracker = Object.assign(
|
||||
new ChangeTracker(workflow, structuredClone(defaultGraph)),
|
||||
{
|
||||
checkState: vi.fn() as Mock
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const workflowOverrides = {
|
||||
changeTracker,
|
||||
...overrides
|
||||
} as Partial<LoadedComfyWorkflow> as LoadedComfyWorkflow
|
||||
} satisfies Partial<LoadedComfyWorkflow>
|
||||
|
||||
return Object.assign(workflow, workflowOverrides) as LoadedComfyWorkflow
|
||||
}
|
||||
|
||||
// Mock the litegraph module
|
||||
@@ -110,12 +128,14 @@ describe('ColorPickerButton', () => {
|
||||
const wrapper = createWrapper()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(wrapper.find('.color-picker-container').exists()).toBe(false)
|
||||
expect(wrapper.findComponent({ name: 'SelectButton' }).exists()).toBe(false)
|
||||
|
||||
await button.trigger('click')
|
||||
expect(wrapper.find('.color-picker-container').exists()).toBe(true)
|
||||
const picker = wrapper.findComponent({ name: 'SelectButton' })
|
||||
expect(picker.exists()).toBe(true)
|
||||
expect(picker.findAll('button').length).toBeGreaterThan(0)
|
||||
|
||||
await button.trigger('click')
|
||||
expect(wrapper.find('.color-picker-container').exists()).toBe(false)
|
||||
expect(wrapper.findComponent({ name: 'SelectButton' }).exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,13 +11,17 @@
|
||||
@click="() => (showColorPicker = !showColorPicker)"
|
||||
>
|
||||
<div class="flex items-center gap-1 px-0">
|
||||
<i class="pi pi-circle-fill" :style="{ color: currentColor ?? '' }" />
|
||||
<i
|
||||
class="pi pi-circle-fill"
|
||||
data-testid="color-picker-current-color"
|
||||
:style="{ color: currentColor ?? '' }"
|
||||
/>
|
||||
<i class="icon-[lucide--chevron-down]" />
|
||||
</div>
|
||||
</Button>
|
||||
<div
|
||||
v-if="showColorPicker"
|
||||
class="color-picker-container absolute -top-10 left-1/2"
|
||||
class="absolute -top-10 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
<SelectButton
|
||||
:model-value="selectedColorOption"
|
||||
@@ -159,13 +163,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../assets/css/style.css';
|
||||
|
||||
.color-picker-container {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) {
|
||||
@apply py-2 px-1;
|
||||
padding: calc(var(--spacing) * 2) var(--spacing);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
<div
|
||||
v-show="widgetState.visible"
|
||||
ref="widgetElement"
|
||||
class="dom-widget"
|
||||
class="dom-widget h-full w-full"
|
||||
:title="tooltip"
|
||||
:style="style"
|
||||
>
|
||||
<component
|
||||
:is="widget.component"
|
||||
v-if="isComponentWidget(widget)"
|
||||
class="h-full w-full"
|
||||
:model-value="widget.value"
|
||||
:widget="widget"
|
||||
v-bind="widget.props"
|
||||
@@ -174,6 +175,8 @@ const mountElementIfVisible = () => {
|
||||
if (widgetElement.value.contains(widget.element)) {
|
||||
return
|
||||
}
|
||||
|
||||
widget.element.classList.add('h-full', 'w-full')
|
||||
widgetElement.value.appendChild(widget.element)
|
||||
}
|
||||
|
||||
@@ -196,11 +199,3 @@ watch(
|
||||
|
||||
whenever(() => !canvasStore.linearMode, mountElementIfVisible)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../assets/css/style.css';
|
||||
|
||||
.dom-widget > * {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
<!-- Markdown fetched successfully -->
|
||||
<div
|
||||
v-else-if="!error"
|
||||
class="markdown-content"
|
||||
class="markdown-content overflow-visible text-sm leading-(--text-sm--line-height)"
|
||||
v-html="renderedHelpHtml"
|
||||
/>
|
||||
<!-- Fallback: markdown not found or fetch error -->
|
||||
<div v-else class="fallback-content space-y-6 text-sm">
|
||||
<div
|
||||
v-else
|
||||
class="fallback-content space-y-6 text-sm leading-(--text-sm--line-height)"
|
||||
>
|
||||
<p v-if="node.description">
|
||||
<strong>{{ $t('g.description') }}:</strong> {{ node.description }}
|
||||
</p>
|
||||
@@ -22,48 +25,52 @@
|
||||
<strong>{{ $t('nodeHelpPage.inputs') }}:</strong>
|
||||
</p>
|
||||
<!-- Using plain HTML table instead of DataTable for consistent styling with markdown content -->
|
||||
<table class="overflow-x-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('g.name') }}</th>
|
||||
<th>{{ $t('nodeHelpPage.type') }}</th>
|
||||
<th>{{ $t('g.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="input in inputList" :key="input.name">
|
||||
<td>
|
||||
<code>{{ input.name }}</code>
|
||||
</td>
|
||||
<td>{{ input.type }}</td>
|
||||
<td>{{ input.tooltip || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('g.name') }}</th>
|
||||
<th>{{ $t('nodeHelpPage.type') }}</th>
|
||||
<th>{{ $t('g.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="input in inputList" :key="input.name">
|
||||
<td>
|
||||
<code>{{ input.name }}</code>
|
||||
</td>
|
||||
<td>{{ input.type }}</td>
|
||||
<td>{{ input.tooltip || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="outputList.length">
|
||||
<p>
|
||||
<strong>{{ $t('nodeHelpPage.outputs') }}:</strong>
|
||||
</p>
|
||||
<table class="overflow-x-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('g.name') }}</th>
|
||||
<th>{{ $t('nodeHelpPage.type') }}</th>
|
||||
<th>{{ $t('g.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="output in outputList" :key="output.name">
|
||||
<td>
|
||||
<code>{{ output.name }}</code>
|
||||
</td>
|
||||
<td>{{ output.type }}</td>
|
||||
<td>{{ output.tooltip || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('g.name') }}</th>
|
||||
<th>{{ $t('nodeHelpPage.type') }}</th>
|
||||
<th>{{ $t('g.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="output in outputList" :key="output.name">
|
||||
<td>
|
||||
<code>{{ output.name }}</code>
|
||||
</td>
|
||||
<td>{{ output.type }}</td>
|
||||
<td>{{ output.tooltip || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,39 +107,59 @@ const outputList = computed(() =>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference './../../assets/css/style.css';
|
||||
|
||||
.node-help-content :deep(:is(img, video)) {
|
||||
@apply max-w-full h-auto block mb-4;
|
||||
}
|
||||
|
||||
.markdown-content,
|
||||
.fallback-content {
|
||||
@apply text-sm overflow-visible;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h1),
|
||||
.fallback-content h1 {
|
||||
@apply text-[22px] font-bold mt-8 mb-4 first:mt-0;
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h2),
|
||||
.fallback-content h2 {
|
||||
@apply text-[18px] font-bold mt-8 mb-4 first:mt-0;
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h3),
|
||||
.fallback-content h3 {
|
||||
@apply text-[16px] font-bold mt-8 mb-4 first:mt-0;
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h4),
|
||||
.fallback-content h4 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h5),
|
||||
.fallback-content h5 {
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.markdown-content :deep(h6),
|
||||
.fallback-content h4,
|
||||
.fallback-content h5,
|
||||
.fallback-content h6 {
|
||||
@apply mt-8 mb-4 first:mt-0;
|
||||
margin-top: calc(var(--spacing) * 8);
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.markdown-content :deep(td),
|
||||
@@ -155,7 +182,8 @@ const outputList = computed(() =>
|
||||
.markdown-content :deep(ol),
|
||||
.fallback-content ul,
|
||||
.fallback-content ol {
|
||||
@apply pl-8 my-2;
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
padding-left: calc(var(--spacing) * 8);
|
||||
}
|
||||
|
||||
.markdown-content :deep(ul ul),
|
||||
@@ -166,36 +194,42 @@ const outputList = computed(() =>
|
||||
.fallback-content ol ol,
|
||||
.fallback-content ul ol,
|
||||
.fallback-content ol ul {
|
||||
@apply pl-6 my-2;
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
padding-left: calc(var(--spacing) * 6);
|
||||
}
|
||||
|
||||
.markdown-content :deep(li),
|
||||
.fallback-content li {
|
||||
@apply my-2;
|
||||
margin-block: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.markdown-content :deep(*:first-child),
|
||||
.fallback-content > *:first-child {
|
||||
@apply mt-0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown-content :deep(code),
|
||||
.fallback-content code {
|
||||
color: var(--code-text-color);
|
||||
background-color: var(--code-bg-color);
|
||||
@apply rounded px-1.5 py-0.5;
|
||||
border-radius: var(--radius);
|
||||
padding: calc(var(--spacing) * 0.5) calc(var(--spacing) * 1.5);
|
||||
}
|
||||
|
||||
.markdown-content :deep(table),
|
||||
.fallback-content table {
|
||||
@apply w-full border-collapse;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.fallback-content table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-content :deep(th),
|
||||
.markdown-content :deep(td),
|
||||
.fallback-content th,
|
||||
.fallback-content td {
|
||||
@apply px-2 py-2;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.markdown-content :deep(tr),
|
||||
@@ -215,16 +249,22 @@ const outputList = computed(() =>
|
||||
|
||||
.markdown-content :deep(pre),
|
||||
.fallback-content pre {
|
||||
@apply rounded p-4 my-4 overflow-x-auto;
|
||||
margin-block: calc(var(--spacing) * 4);
|
||||
overflow-x: auto;
|
||||
border-radius: var(--radius);
|
||||
padding: calc(var(--spacing) * 4);
|
||||
background-color: var(--code-block-bg-color);
|
||||
|
||||
code {
|
||||
@apply bg-transparent p-0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
color: var(--p-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-content :deep(table) {
|
||||
@apply overflow-x-auto;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="_content">
|
||||
<div class="flex flex-col gap-2">
|
||||
<SelectButton
|
||||
v-model="selectedFilter"
|
||||
class="filter-type-select"
|
||||
@@ -16,7 +16,7 @@
|
||||
auto-filter-focus
|
||||
/>
|
||||
</div>
|
||||
<div class="_footer">
|
||||
<div class="flex flex-col items-end pt-4">
|
||||
<Button type="button" @click="submit">{{ $t('g.add') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -67,15 +67,3 @@ const submit = () => {
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
._content {
|
||||
@apply flex flex-col space-y-2;
|
||||
}
|
||||
|
||||
._footer {
|
||||
@apply flex flex-col pt-4 items-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -255,8 +255,6 @@ onMounted(() => {
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.floating-sidebar {
|
||||
padding: var(--sidebar-padding);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
:aria-label="computedTooltip"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<div class="side-bar-button-content">
|
||||
<div class="side-bar-button-content flex flex-col items-center gap-2">
|
||||
<slot name="icon">
|
||||
<div class="sidebar-icon-wrapper relative">
|
||||
<i
|
||||
@@ -40,9 +40,11 @@
|
||||
</span>
|
||||
</div>
|
||||
</slot>
|
||||
<span v-if="label && !isSmall" class="side-bar-button-label">{{
|
||||
st(label, label)
|
||||
}}</span>
|
||||
<span
|
||||
v-if="label && !isSmall"
|
||||
class="side-bar-button-label text-center text-[10px]"
|
||||
>{{ st(label, label) }}</span
|
||||
>
|
||||
</div>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -104,8 +106,6 @@ const computedTooltip = computed(() => st(tooltip, tooltip) + tooltipSuffix)
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.side-bar-button {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-item-height);
|
||||
@@ -117,12 +117,7 @@ const computedTooltip = computed(() => st(tooltip, tooltip) + tooltipSuffix)
|
||||
height: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.side-bar-button-content {
|
||||
@apply flex flex-col items-center gap-2;
|
||||
}
|
||||
|
||||
.side-bar-button-label {
|
||||
@apply text-[10px] text-center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<div class="comfy-vue-side-bar-header flex flex-col gap-2">
|
||||
<Toolbar
|
||||
class="min-h-16 bg-transparent rounded-none border-x-0 border-t-0 px-2 2xl:px-4"
|
||||
:pt="sidebarPt"
|
||||
>
|
||||
<template #start>
|
||||
<span class="truncate font-bold" :title="props.title">
|
||||
@@ -20,7 +21,7 @@
|
||||
</template>
|
||||
<template #end>
|
||||
<div
|
||||
class="touch:w-auto touch:opacity-100 flex flex-row overflow-hidden transition-all duration-200 motion-safe:w-0 motion-safe:opacity-0 motion-safe:group-focus-within/sidebar-tab:w-auto motion-safe:group-focus-within/sidebar-tab:opacity-100 motion-safe:group-hover/sidebar-tab:w-auto motion-safe:group-hover/sidebar-tab:opacity-100"
|
||||
class="touch:w-auto touch:opacity-100 [&_.p-button]:py-1 2xl:[&_.p-button]:py-2 flex flex-row overflow-hidden transition-all duration-200 motion-safe:w-0 motion-safe:opacity-0 motion-safe:group-focus-within/sidebar-tab:w-auto motion-safe:group-focus-within/sidebar-tab:opacity-100 motion-safe:group-hover/sidebar-tab:w-auto motion-safe:group-hover/sidebar-tab:opacity-100"
|
||||
>
|
||||
<slot name="tool-buttons" />
|
||||
</div>
|
||||
@@ -54,19 +55,10 @@ const props = defineProps<{
|
||||
title: string
|
||||
class?: string
|
||||
}>()
|
||||
const sidebarPt = {
|
||||
start: 'min-w-0 flex-1 overflow-hidden'
|
||||
}
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
provide(SidebarContainerKey, containerRef)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../assets/css/style.css';
|
||||
|
||||
:deep(.p-toolbar-end) .p-button {
|
||||
@apply py-1 2xl:py-2;
|
||||
}
|
||||
|
||||
:deep(.p-toolbar-start) {
|
||||
@apply min-w-0 flex-1 overflow-hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="node-lib-node-container"
|
||||
class="node-lib-node-container h-full w-full"
|
||||
data-testid="node-tree-leaf"
|
||||
:data-node-name="nodeDef.display_name"
|
||||
>
|
||||
@@ -206,11 +206,3 @@ onUnmounted(() => {
|
||||
nodeContentElement.value?.removeEventListener('mouseleave', handleMouseLeave)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../../../assets/css/style.css';
|
||||
|
||||
.node-lib-node-container {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -140,54 +140,65 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-preview-content {
|
||||
@apply flex flex-col rounded-xl overflow-hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-xl);
|
||||
max-width: var(--popover-width);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.workflow-preview-thumbnail {
|
||||
@apply relative p-2;
|
||||
position: relative;
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.workflow-preview-thumbnail img {
|
||||
@apply shadow-md;
|
||||
box-shadow: var(--shadow-md);
|
||||
background-color: color-mix(in srgb, var(--comfy-menu-bg) 70%, black);
|
||||
}
|
||||
|
||||
.dark-theme .workflow-preview-thumbnail img {
|
||||
@apply shadow-lg;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.workflow-preview-footer {
|
||||
@apply pt-1 pb-2 px-3;
|
||||
padding-top: calc(var(--spacing) * 1);
|
||||
padding-right: calc(var(--spacing) * 3);
|
||||
padding-bottom: calc(var(--spacing) * 2);
|
||||
padding-left: calc(var(--spacing) * 3);
|
||||
}
|
||||
|
||||
.workflow-preview-name {
|
||||
@apply block text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--fg-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-popover-fade {
|
||||
--p-popover-background: transparent;
|
||||
--p-popover-content-padding: 0;
|
||||
@apply bg-transparent rounded-xl shadow-lg;
|
||||
border-radius: var(--radius-xl);
|
||||
background-color: transparent;
|
||||
box-shadow: var(--shadow-lg);
|
||||
transition: opacity 0.15s ease-out !important;
|
||||
}
|
||||
|
||||
.workflow-popover-fade.p-popover-flipped {
|
||||
@apply -translate-y-full;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.dark-theme .workflow-popover-fade {
|
||||
@apply shadow-2xl;
|
||||
box-shadow: var(--shadow-2xl);
|
||||
}
|
||||
|
||||
.workflow-popover-fade.p-popover::after,
|
||||
|
||||
@@ -300,63 +300,72 @@ onUpdated(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
.workflow-tabs-container {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) {
|
||||
@apply p-0 bg-transparent rounded-none shrink relative border-0 border-r border-solid;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
border: 0;
|
||||
border-right-style: solid;
|
||||
border-right-width: 1px;
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-right-color: var(--border-color);
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.overflow-arrow {
|
||||
@apply px-2 rounded-none;
|
||||
border-radius: 0;
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
|
||||
.overflow-arrow[disabled] {
|
||||
@apply opacity-25;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton > .p-togglebutton-content) {
|
||||
@apply max-w-full;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.workflow-tab) {
|
||||
@apply max-w-full;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton::before) {
|
||||
@apply hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:first-child) {
|
||||
@apply border-l border-solid;
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
border-left-color: var(--border-color);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:not(:first-child)) {
|
||||
@apply border-l-0;
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton.p-togglebutton-checked) {
|
||||
@apply border-b border-solid h-full;
|
||||
height: 100%;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: var(--p-button-text-primary-color);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:not(.p-togglebutton-checked)) {
|
||||
@apply opacity-75;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton-checked) .close-button,
|
||||
:deep(.p-togglebutton:hover) .close-button {
|
||||
@apply visible;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
:deep(.p-scrollpanel-content) {
|
||||
@apply h-full;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.workflow-tabs) {
|
||||
@@ -366,11 +375,12 @@ onUpdated(() => {
|
||||
/* Scrollbar half opacity to avoid blocking the active tab bottom border */
|
||||
:deep(.p-scrollpanel:hover .p-scrollpanel-bar),
|
||||
:deep(.p-scrollpanel:active .p-scrollpanel-bar) {
|
||||
@apply opacity-50;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:deep(.p-selectbutton) {
|
||||
@apply rounded-none h-full;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.workflow-tabs-container-desktop {
|
||||
@@ -378,7 +388,7 @@ onUpdated(() => {
|
||||
}
|
||||
|
||||
.window-actions-spacer {
|
||||
@apply flex-auto;
|
||||
flex: auto;
|
||||
/* If we are using custom titlebar, then we need to add a gap for the user to drag the window */
|
||||
--window-actions-spacer-width: min(75px, env(titlebar-area-width, 0) * 9999);
|
||||
min-width: var(--window-actions-spacer-width);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"empty": "Empty",
|
||||
"noWorkflowsFound": "No workflows found.",
|
||||
"comingSoon": "Coming Soon",
|
||||
"inSubgraph": "in subgraph {name}",
|
||||
"download": "Download",
|
||||
"downloadImage": "Download image",
|
||||
"downloadVideo": "Download video",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useMissingModelsDialog } from '@/composables/useMissingModelsDialog'
|
||||
import { useMissingNodesDialog } from '@/composables/useMissingNodesDialog'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
|
||||
@@ -33,6 +34,7 @@ export const useWorkflowService = () => {
|
||||
const missingNodesDialog = useMissingNodesDialog()
|
||||
const workflowThumbnail = useWorkflowThumbnail()
|
||||
const domWidgetStore = useDomWidgetStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const workflowDraftStore = useWorkflowDraftStore()
|
||||
|
||||
async function getFilename(defaultName: string): Promise<string | null> {
|
||||
@@ -472,7 +474,15 @@ export const useWorkflowService = () => {
|
||||
settingStore.get('Comfy.Workflow.ShowMissingNodesWarning')
|
||||
) {
|
||||
missingNodesDialog.show({ missingNodeTypes })
|
||||
|
||||
// For now, we'll make them coexist.
|
||||
// Once the Node Replacement feature is implemented in TabErrors
|
||||
// we'll remove the modal display and direct users to the error tab.
|
||||
if (settingStore.get('Comfy.RightSidePanel.ShowErrorsTab')) {
|
||||
executionErrorStore.showErrorOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
missingModels &&
|
||||
settingStore.get('Comfy.Workflow.ShowMissingModelsWarning')
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from 'fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { validateComfyWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import {
|
||||
buildSubgraphExecutionPaths,
|
||||
validateComfyWorkflow
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { defaultGraph } from '@/scripts/defaultGraph'
|
||||
|
||||
const WORKFLOW_DIR = 'src/platform/workflow/validation/schemas/__fixtures__'
|
||||
@@ -206,3 +210,67 @@ describe('parseComfyWorkflow', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function node(id: number, type: string): ComfyNode {
|
||||
return { id, type } as ComfyNode
|
||||
}
|
||||
|
||||
function subgraphDef(id: string, nodes: ComfyNode[]) {
|
||||
return { id, name: id, nodes, inputNode: {}, outputNode: {} }
|
||||
}
|
||||
|
||||
describe('buildSubgraphExecutionPaths', () => {
|
||||
it('returns empty map when there are no subgraph definitions', () => {
|
||||
expect(buildSubgraphExecutionPaths([node(5, 'SomeNode')], [])).toEqual(
|
||||
new Map()
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty map when no root node matches a subgraph type', () => {
|
||||
const def = subgraphDef('def-A', [])
|
||||
expect(
|
||||
buildSubgraphExecutionPaths([node(5, 'UnrelatedNode')], [def])
|
||||
).toEqual(new Map())
|
||||
})
|
||||
|
||||
it('maps a single subgraph instance to its execution path', () => {
|
||||
const def = subgraphDef('def-A', [])
|
||||
const result = buildSubgraphExecutionPaths([node(5, 'def-A')], [def])
|
||||
expect(result.get('def-A')).toEqual(['5'])
|
||||
})
|
||||
|
||||
it('collects multiple instances of the same subgraph type', () => {
|
||||
const def = subgraphDef('def-A', [])
|
||||
const result = buildSubgraphExecutionPaths(
|
||||
[node(5, 'def-A'), node(10, 'def-A')],
|
||||
[def]
|
||||
)
|
||||
expect(result.get('def-A')).toEqual(['5', '10'])
|
||||
})
|
||||
|
||||
it('builds nested execution paths for subgraphs within subgraphs', () => {
|
||||
const innerDef = subgraphDef('def-B', [])
|
||||
const outerDef = subgraphDef('def-A', [node(70, 'def-B')])
|
||||
const result = buildSubgraphExecutionPaths(
|
||||
[node(5, 'def-A')],
|
||||
[outerDef, innerDef]
|
||||
)
|
||||
expect(result.get('def-A')).toEqual(['5'])
|
||||
expect(result.get('def-B')).toEqual(['5:70'])
|
||||
})
|
||||
|
||||
it('does not recurse infinitely on self-referential subgraph definitions', () => {
|
||||
const cyclicDef = subgraphDef('def-A', [node(70, 'def-A')])
|
||||
expect(() =>
|
||||
buildSubgraphExecutionPaths([node(5, 'def-A')], [cyclicDef])
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not recurse infinitely on mutually cyclic subgraph definitions', () => {
|
||||
const defA = subgraphDef('def-A', [node(70, 'def-B')])
|
||||
const defB = subgraphDef('def-B', [node(80, 'def-A')])
|
||||
expect(() =>
|
||||
buildSubgraphExecutionPaths([node(5, 'def-A')], [defA, defB])
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -541,3 +541,46 @@ const zNodeData = z.object({
|
||||
|
||||
const zComfyApiWorkflow = z.record(zNodeId, zNodeData)
|
||||
export type ComfyApiWorkflow = z.infer<typeof zComfyApiWorkflow>
|
||||
|
||||
/**
|
||||
* Builds a map from subgraph definition ID to all execution path prefixes
|
||||
* where that definition is instantiated in the workflow.
|
||||
*
|
||||
* "def-A" → ["5", "10"] for each container node instantiating that subgraph definition.
|
||||
*/
|
||||
export function buildSubgraphExecutionPaths(
|
||||
rootNodes: ComfyNode[],
|
||||
allSubgraphDefs: unknown[]
|
||||
): Map<string, string[]> {
|
||||
const subgraphDefMap = new Map(
|
||||
allSubgraphDefs.filter(isSubgraphDefinition).map((s) => [s.id, s])
|
||||
)
|
||||
const pathMap = new Map<string, string[]>()
|
||||
const visited = new Set<string>()
|
||||
|
||||
const build = (nodes: ComfyNode[], parentPrefix: string) => {
|
||||
for (const n of nodes ?? []) {
|
||||
if (typeof n.type !== 'string' || !subgraphDefMap.has(n.type)) continue
|
||||
const path = parentPrefix ? `${parentPrefix}:${n.id}` : String(n.id)
|
||||
const existing = pathMap.get(n.type)
|
||||
if (existing) {
|
||||
existing.push(path)
|
||||
} else {
|
||||
pathMap.set(n.type, [path])
|
||||
}
|
||||
|
||||
if (visited.has(n.type)) continue
|
||||
visited.add(n.type)
|
||||
|
||||
const innerDef = subgraphDefMap.get(n.type)
|
||||
if (innerDef) {
|
||||
build(innerDef.nodes, path)
|
||||
}
|
||||
|
||||
visited.delete(n.type)
|
||||
}
|
||||
}
|
||||
|
||||
build(rootNodes, '')
|
||||
return pathMap
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ import {
|
||||
type ComfyWorkflowJSON,
|
||||
type ModelFile,
|
||||
type NodeId,
|
||||
isSubgraphDefinition
|
||||
isSubgraphDefinition,
|
||||
buildSubgraphExecutionPaths
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
ExecutionErrorWsMessage,
|
||||
@@ -1099,6 +1100,15 @@ export class ComfyApp {
|
||||
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
|
||||
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||
useMissingNodesDialog().show({ missingNodeTypes })
|
||||
|
||||
// For now, we'll make them coexist.
|
||||
// Once the Node Replacement feature is implemented in TabErrors
|
||||
// we'll remove the modal display and direct users to the error tab.
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.setMissingNodeTypes(missingNodeTypes)
|
||||
if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) {
|
||||
executionErrorStore.showErrorOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1181,12 +1191,13 @@ export class ComfyApp {
|
||||
|
||||
const collectMissingNodesAndModels = (
|
||||
nodes: ComfyWorkflowJSON['nodes'],
|
||||
path: string = ''
|
||||
pathPrefix: string = '',
|
||||
displayName: string = ''
|
||||
) => {
|
||||
if (!Array.isArray(nodes)) {
|
||||
console.warn(
|
||||
'Workflow nodes data is missing or invalid, skipping node processing',
|
||||
{ nodes, path }
|
||||
{ nodes, pathPrefix }
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -1195,9 +1206,26 @@ export class ComfyApp {
|
||||
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||
const replacement = nodeReplacementStore.getReplacementFor(n.type)
|
||||
|
||||
// To access missing node information in the error tab
|
||||
// we collect the cnr_id and execution_id here.
|
||||
let cnrId: string | undefined
|
||||
if (typeof n.properties?.cnr_id === 'string') {
|
||||
cnrId = n.properties.cnr_id
|
||||
} else if (typeof n.properties?.aux_id === 'string') {
|
||||
cnrId = n.properties.aux_id
|
||||
}
|
||||
|
||||
const executionId = pathPrefix
|
||||
? `${pathPrefix}:${n.id}`
|
||||
: String(n.id)
|
||||
|
||||
missingNodeTypes.push({
|
||||
type: n.type,
|
||||
...(path && { hint: `in subgraph '${path}'` }),
|
||||
nodeId: executionId,
|
||||
cnrId,
|
||||
...(displayName && {
|
||||
hint: t('g.inSubgraph', { name: displayName })
|
||||
}),
|
||||
isReplaceable: replacement !== null,
|
||||
replacement: replacement ?? undefined
|
||||
})
|
||||
@@ -1216,14 +1244,25 @@ export class ComfyApp {
|
||||
// Process nodes at the top level
|
||||
collectMissingNodesAndModels(graphData.nodes)
|
||||
|
||||
// Build map: subgraph definition UUID → full execution path prefix.
|
||||
// Handles arbitrary nesting depth (e.g. root node 11 → "11", node 14 in sg 11 → "11:14").
|
||||
const subgraphContainerIdMap = buildSubgraphExecutionPaths(
|
||||
graphData.nodes,
|
||||
graphData.definitions?.subgraphs ?? []
|
||||
)
|
||||
|
||||
// Process nodes in subgraphs
|
||||
if (graphData.definitions?.subgraphs) {
|
||||
for (const subgraph of graphData.definitions.subgraphs) {
|
||||
if (isSubgraphDefinition(subgraph)) {
|
||||
collectMissingNodesAndModels(
|
||||
subgraph.nodes,
|
||||
subgraph.name || subgraph.id
|
||||
)
|
||||
const paths = subgraphContainerIdMap.get(subgraph.id) ?? []
|
||||
for (const pathPrefix of paths) {
|
||||
collectMissingNodesAndModels(
|
||||
subgraph.nodes,
|
||||
pathPrefix,
|
||||
subgraph.name || subgraph.id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { st } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -10,8 +12,13 @@ import type {
|
||||
PromptError
|
||||
} from '@/schemas/apiSchema'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
getAncestorExecutionIds,
|
||||
getParentExecutionIds
|
||||
} from '@/types/nodeIdentification'
|
||||
import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import {
|
||||
executionIdToNodeLocatorId,
|
||||
forEachNode,
|
||||
@@ -20,12 +27,52 @@ import {
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
|
||||
/**
|
||||
* Store dedicated to execution error state management.
|
||||
*
|
||||
* Extracted from executionStore to separate error-related concerns
|
||||
* (state, computed properties, graph flag propagation, overlay UI)
|
||||
* from execution flow management (progress, queuing, events).
|
||||
* @knipIgnoreUsedByStackedPR
|
||||
*/
|
||||
interface MissingNodesError {
|
||||
message: string
|
||||
nodeTypes: MissingNodeType[]
|
||||
}
|
||||
|
||||
function clearAllNodeErrorFlags(rootGraph: LGraph): void {
|
||||
forEachNode(rootGraph, (node) => {
|
||||
node.has_errors = false
|
||||
if (node.inputs) {
|
||||
for (const slot of node.inputs) {
|
||||
slot.hasErrors = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function markNodeSlotErrors(node: LGraphNode, nodeError: NodeError): void {
|
||||
if (!node.inputs) return
|
||||
for (const error of nodeError.errors) {
|
||||
const slotName = error.extra_info?.input_name
|
||||
if (!slotName) continue
|
||||
const slot = node.inputs.find((s) => s.name === slotName)
|
||||
if (slot) slot.hasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
function applyNodeError(
|
||||
rootGraph: LGraph,
|
||||
executionId: NodeExecutionId,
|
||||
nodeError: NodeError
|
||||
): void {
|
||||
const node = getNodeByExecutionId(rootGraph, executionId)
|
||||
if (!node) return
|
||||
|
||||
node.has_errors = true
|
||||
markNodeSlotErrors(node, nodeError)
|
||||
|
||||
for (const parentId of getParentExecutionIds(executionId)) {
|
||||
const parentNode = getNodeByExecutionId(rootGraph, parentId)
|
||||
if (parentNode) parentNode.has_errors = true
|
||||
}
|
||||
}
|
||||
|
||||
/** Execution error state: node errors, runtime errors, prompt errors, and missing nodes. */
|
||||
export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -33,6 +80,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
const lastNodeErrors = ref<Record<NodeId, NodeError> | null>(null)
|
||||
const lastExecutionError = ref<ExecutionErrorWsMessage | null>(null)
|
||||
const lastPromptError = ref<PromptError | null>(null)
|
||||
const missingNodesError = ref<MissingNodesError | null>(null)
|
||||
|
||||
const isErrorOverlayOpen = ref(false)
|
||||
|
||||
@@ -49,6 +97,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
lastExecutionError.value = null
|
||||
lastPromptError.value = null
|
||||
lastNodeErrors.value = null
|
||||
missingNodesError.value = null
|
||||
isErrorOverlayOpen.value = false
|
||||
}
|
||||
|
||||
@@ -57,6 +106,40 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
lastPromptError.value = null
|
||||
}
|
||||
|
||||
function setMissingNodeTypes(types: MissingNodeType[]) {
|
||||
if (!types.length) {
|
||||
missingNodesError.value = null
|
||||
return
|
||||
}
|
||||
const seen = new Set<string>()
|
||||
const uniqueTypes = types.filter((node) => {
|
||||
// For string entries (group nodes), deduplicate by the string itself.
|
||||
// For object entries, prefer nodeId so multiple instances of the same
|
||||
// type are kept as separate rows; fall back to type if nodeId is absent.
|
||||
const isString = typeof node === 'string'
|
||||
let key: string
|
||||
if (isString) {
|
||||
key = node
|
||||
} else if (node.nodeId != null) {
|
||||
key = String(node.nodeId)
|
||||
} else {
|
||||
key = node.type
|
||||
}
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
missingNodesError.value = {
|
||||
message: isCloud
|
||||
? st(
|
||||
'rightSidePanel.missingNodePacks.unsupportedTitle',
|
||||
'Unsupported Node Packs'
|
||||
)
|
||||
: st('rightSidePanel.missingNodePacks.title', 'Missing Node Packs'),
|
||||
nodeTypes: uniqueTypes
|
||||
}
|
||||
}
|
||||
|
||||
const lastExecutionErrorNodeLocatorId = computed(() => {
|
||||
const err = lastExecutionError.value
|
||||
if (!err) return null
|
||||
@@ -81,9 +164,18 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
() => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0
|
||||
)
|
||||
|
||||
/** Whether any error (node validation, runtime execution, or prompt-level) is present */
|
||||
/** Whether any missing node types are present in the current workflow
|
||||
* @knipIgnoreUsedByStackedPR
|
||||
*/
|
||||
const hasMissingNodes = computed(() => !!missingNodesError.value)
|
||||
|
||||
/** Whether any error (node validation, runtime execution, prompt-level, or missing nodes) is present */
|
||||
const hasAnyError = computed(
|
||||
() => hasExecutionError.value || hasPromptError.value || hasNodeError.value
|
||||
() =>
|
||||
hasExecutionError.value ||
|
||||
hasPromptError.value ||
|
||||
hasNodeError.value ||
|
||||
hasMissingNodes.value
|
||||
)
|
||||
|
||||
const allErrorExecutionIds = computed<string[]>(() => {
|
||||
@@ -116,13 +208,19 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
/** Count of runtime execution errors (0 or 1) */
|
||||
const executionErrorCount = computed(() => (lastExecutionError.value ? 1 : 0))
|
||||
|
||||
/** Count of missing node errors (0 or 1) */
|
||||
const missingNodeCount = computed(() => (missingNodesError.value ? 1 : 0))
|
||||
|
||||
/** Total count of all individual errors */
|
||||
const totalErrorCount = computed(
|
||||
() =>
|
||||
promptErrorCount.value + nodeErrorCount.value + executionErrorCount.value
|
||||
promptErrorCount.value +
|
||||
nodeErrorCount.value +
|
||||
executionErrorCount.value +
|
||||
missingNodeCount.value
|
||||
)
|
||||
|
||||
/** Pre-computed Set of graph node IDs (as strings) that have errors in the current graph scope. */
|
||||
/** Graph node IDs (as strings) that have errors in the current graph scope. */
|
||||
const activeGraphErrorNodeIds = computed<Set<string>>(() => {
|
||||
const ids = new Set<string>()
|
||||
if (!app.rootGraph) return ids
|
||||
@@ -150,6 +248,44 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
return ids
|
||||
})
|
||||
|
||||
/**
|
||||
* Set of all execution ID prefixes derived from missing node execution IDs,
|
||||
* including the missing nodes themselves.
|
||||
*
|
||||
* Example: missing node at "65:70:63" → Set { "65", "65:70", "65:70:63" }
|
||||
*/
|
||||
const missingAncestorExecutionIds = computed<Set<NodeExecutionId>>(() => {
|
||||
const ids = new Set<NodeExecutionId>()
|
||||
const error = missingNodesError.value
|
||||
if (!error) return ids
|
||||
|
||||
for (const nodeType of error.nodeTypes) {
|
||||
if (typeof nodeType === 'string') continue
|
||||
if (nodeType.nodeId == null) continue
|
||||
for (const id of getAncestorExecutionIds(String(nodeType.nodeId))) {
|
||||
ids.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
})
|
||||
|
||||
const activeMissingNodeGraphIds = computed<Set<string>>(() => {
|
||||
const ids = new Set<string>()
|
||||
if (!app.rootGraph) return ids
|
||||
|
||||
const activeGraph = canvasStore.currentGraph ?? app.rootGraph
|
||||
|
||||
for (const executionId of missingAncestorExecutionIds.value) {
|
||||
const graphNode = getNodeByExecutionId(app.rootGraph, executionId)
|
||||
if (graphNode?.graph === activeGraph) {
|
||||
ids.add(String(graphNode.id))
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
})
|
||||
|
||||
/** Map of node errors indexed by locator ID. */
|
||||
const nodeErrorsByLocatorId = computed<Record<NodeLocatorId, NodeError>>(
|
||||
() => {
|
||||
@@ -196,15 +332,11 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
*/
|
||||
const errorAncestorExecutionIds = computed<Set<NodeExecutionId>>(() => {
|
||||
const ids = new Set<NodeExecutionId>()
|
||||
|
||||
for (const executionId of allErrorExecutionIds.value) {
|
||||
const parts = executionId.split(':')
|
||||
// Add every prefix including the full ID (error leaf node itself)
|
||||
for (let i = 1; i <= parts.length; i++) {
|
||||
ids.add(parts.slice(0, i).join(':'))
|
||||
for (const id of getAncestorExecutionIds(executionId)) {
|
||||
ids.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
})
|
||||
|
||||
@@ -216,59 +348,28 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
return errorAncestorExecutionIds.value.has(execId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update node and slot error flags when validation errors change.
|
||||
* Propagates errors up subgraph chains.
|
||||
/** True if the node has a missing node inside it at any nesting depth.
|
||||
* @knipIgnoreUsedByStackedPR
|
||||
*/
|
||||
watch(lastNodeErrors, () => {
|
||||
if (!app.rootGraph) return
|
||||
function isContainerWithMissingNode(node: LGraphNode): boolean {
|
||||
if (!app.rootGraph) return false
|
||||
const execId = getExecutionIdByNode(app.rootGraph, node)
|
||||
if (!execId) return false
|
||||
return missingAncestorExecutionIds.value.has(execId)
|
||||
}
|
||||
|
||||
// Clear all error flags
|
||||
forEachNode(app.rootGraph, (node) => {
|
||||
node.has_errors = false
|
||||
if (node.inputs) {
|
||||
for (const slot of node.inputs) {
|
||||
slot.hasErrors = false
|
||||
}
|
||||
}
|
||||
})
|
||||
watch(lastNodeErrors, () => {
|
||||
const rootGraph = app.rootGraph
|
||||
if (!rootGraph) return
|
||||
|
||||
clearAllNodeErrorFlags(rootGraph)
|
||||
|
||||
if (!lastNodeErrors.value) return
|
||||
|
||||
// Set error flags on nodes and slots
|
||||
for (const [executionId, nodeError] of Object.entries(
|
||||
lastNodeErrors.value
|
||||
)) {
|
||||
const node = getNodeByExecutionId(app.rootGraph, executionId)
|
||||
if (!node) continue
|
||||
|
||||
node.has_errors = true
|
||||
|
||||
// Mark input slots with errors
|
||||
if (node.inputs) {
|
||||
for (const error of nodeError.errors) {
|
||||
const slotName = error.extra_info?.input_name
|
||||
if (!slotName) continue
|
||||
|
||||
const slot = node.inputs.find((s) => s.name === slotName)
|
||||
if (slot) {
|
||||
slot.hasErrors = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate errors to parent subgraph nodes
|
||||
const parts = executionId.split(':')
|
||||
for (let i = parts.length - 1; i > 0; i--) {
|
||||
const parentExecutionId = parts.slice(0, i).join(':')
|
||||
const parentNode = getNodeByExecutionId(
|
||||
app.rootGraph,
|
||||
parentExecutionId
|
||||
)
|
||||
if (parentNode) {
|
||||
parentNode.has_errors = true
|
||||
}
|
||||
}
|
||||
applyNodeError(rootGraph, executionId, nodeError)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -277,6 +378,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
lastNodeErrors,
|
||||
lastExecutionError,
|
||||
lastPromptError,
|
||||
missingNodesError,
|
||||
|
||||
// Clearing
|
||||
clearAllErrors,
|
||||
@@ -291,16 +393,21 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
||||
hasExecutionError,
|
||||
hasPromptError,
|
||||
hasNodeError,
|
||||
hasMissingNodes,
|
||||
hasAnyError,
|
||||
allErrorExecutionIds,
|
||||
totalErrorCount,
|
||||
lastExecutionErrorNodeId,
|
||||
activeGraphErrorNodeIds,
|
||||
activeMissingNodeGraphIds,
|
||||
|
||||
// Missing node actions
|
||||
setMissingNodeTypes,
|
||||
|
||||
// Lookup helpers
|
||||
getNodeErrors,
|
||||
slotHasError,
|
||||
errorAncestorExecutionIds,
|
||||
isContainerWithInternalError
|
||||
isContainerWithInternalError,
|
||||
isContainerWithMissingNode
|
||||
}
|
||||
})
|
||||
|
||||
@@ -90,6 +90,8 @@ export type MissingNodeType =
|
||||
// Primarily used by group nodes.
|
||||
| {
|
||||
type: string
|
||||
nodeId?: string | number
|
||||
cnrId?: string
|
||||
hint?: string
|
||||
action?: {
|
||||
text: string
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSche
|
||||
import {
|
||||
createNodeExecutionId,
|
||||
createNodeLocatorId,
|
||||
getAncestorExecutionIds,
|
||||
getParentExecutionIds,
|
||||
isNodeExecutionId,
|
||||
isNodeLocatorId,
|
||||
parseNodeExecutionId,
|
||||
@@ -204,4 +206,30 @@ describe('nodeIdentification', () => {
|
||||
expect(parsed).toEqual(nodeIds)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAncestorExecutionIds', () => {
|
||||
it('returns only itself for a root node', () => {
|
||||
expect(getAncestorExecutionIds('65')).toEqual(['65'])
|
||||
})
|
||||
|
||||
it('returns all ancestors including self for nested IDs', () => {
|
||||
expect(getAncestorExecutionIds('65:70')).toEqual(['65', '65:70'])
|
||||
expect(getAncestorExecutionIds('65:70:63')).toEqual([
|
||||
'65',
|
||||
'65:70',
|
||||
'65:70:63'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getParentExecutionIds', () => {
|
||||
it('returns empty for a root node', () => {
|
||||
expect(getParentExecutionIds('65')).toEqual([])
|
||||
})
|
||||
|
||||
it('returns all ancestors excluding self for nested IDs', () => {
|
||||
expect(getParentExecutionIds('65:70')).toEqual(['65'])
|
||||
expect(getParentExecutionIds('65:70:63')).toEqual(['65', '65:70'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,3 +121,28 @@ export function parseNodeExecutionId(id: string): NodeId[] | null {
|
||||
export function createNodeExecutionId(nodeIds: NodeId[]): NodeExecutionId {
|
||||
return nodeIds.join(':')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all ancestor execution IDs for a given execution ID, including itself.
|
||||
*
|
||||
* Example: "65:70:63" → ["65", "65:70", "65:70:63"]
|
||||
*/
|
||||
export function getAncestorExecutionIds(
|
||||
executionId: string | NodeExecutionId
|
||||
): NodeExecutionId[] {
|
||||
const parts = executionId.split(':')
|
||||
return Array.from({ length: parts.length }, (_, i) =>
|
||||
parts.slice(0, i + 1).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all ancestor execution IDs for a given execution ID, excluding itself.
|
||||
*
|
||||
* Example: "65:70:63" → ["65", "65:70"]
|
||||
*/
|
||||
export function getParentExecutionIds(
|
||||
executionId: string | NodeExecutionId
|
||||
): NodeExecutionId[] {
|
||||
return getAncestorExecutionIds(executionId).slice(0, -1)
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import {
|
||||
buildSubgraphExecutionPaths,
|
||||
getAncestorExecutionIds,
|
||||
getParentExecutionIds
|
||||
} from '@/utils/executionIdUtil'
|
||||
|
||||
function node(id: number, type: string): ComfyNode {
|
||||
return { id, type } as ComfyNode
|
||||
}
|
||||
|
||||
function subgraphDef(id: string, nodes: ComfyNode[]) {
|
||||
return { id, name: id, nodes, inputNode: {}, outputNode: {} }
|
||||
}
|
||||
|
||||
describe('getAncestorExecutionIds', () => {
|
||||
it('returns only itself for a root node', () => {
|
||||
expect(getAncestorExecutionIds('65')).toEqual(['65'])
|
||||
})
|
||||
|
||||
it('returns all ancestors including self for nested IDs', () => {
|
||||
expect(getAncestorExecutionIds('65:70')).toEqual(['65', '65:70'])
|
||||
expect(getAncestorExecutionIds('65:70:63')).toEqual([
|
||||
'65',
|
||||
'65:70',
|
||||
'65:70:63'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getParentExecutionIds', () => {
|
||||
it('returns empty for a root node', () => {
|
||||
expect(getParentExecutionIds('65')).toEqual([])
|
||||
})
|
||||
|
||||
it('returns all ancestors excluding self for nested IDs', () => {
|
||||
expect(getParentExecutionIds('65:70')).toEqual(['65'])
|
||||
expect(getParentExecutionIds('65:70:63')).toEqual(['65', '65:70'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSubgraphExecutionPaths', () => {
|
||||
it('returns empty map when there are no subgraph definitions', () => {
|
||||
expect(buildSubgraphExecutionPaths([node(5, 'SomeNode')], [])).toEqual(
|
||||
new Map()
|
||||
)
|
||||
})
|
||||
|
||||
it('returns empty map when no root node matches a subgraph type', () => {
|
||||
const def = subgraphDef('def-A', [])
|
||||
expect(
|
||||
buildSubgraphExecutionPaths([node(5, 'UnrelatedNode')], [def])
|
||||
).toEqual(new Map())
|
||||
})
|
||||
|
||||
it('maps a single subgraph instance to its execution path', () => {
|
||||
const def = subgraphDef('def-A', [])
|
||||
const result = buildSubgraphExecutionPaths([node(5, 'def-A')], [def])
|
||||
expect(result.get('def-A')).toEqual(['5'])
|
||||
})
|
||||
|
||||
it('collects multiple instances of the same subgraph type', () => {
|
||||
const def = subgraphDef('def-A', [])
|
||||
const result = buildSubgraphExecutionPaths(
|
||||
[node(5, 'def-A'), node(10, 'def-A')],
|
||||
[def]
|
||||
)
|
||||
expect(result.get('def-A')).toEqual(['5', '10'])
|
||||
})
|
||||
|
||||
it('builds nested execution paths for subgraphs within subgraphs', () => {
|
||||
const innerDef = subgraphDef('def-B', [])
|
||||
const outerDef = subgraphDef('def-A', [node(70, 'def-B')])
|
||||
const result = buildSubgraphExecutionPaths(
|
||||
[node(5, 'def-A')],
|
||||
[outerDef, innerDef]
|
||||
)
|
||||
expect(result.get('def-A')).toEqual(['5'])
|
||||
expect(result.get('def-B')).toEqual(['5:70'])
|
||||
})
|
||||
|
||||
it('does not recurse infinitely on self-referential subgraph definitions', () => {
|
||||
const cyclicDef = subgraphDef('def-A', [node(70, 'def-A')])
|
||||
expect(() =>
|
||||
buildSubgraphExecutionPaths([node(5, 'def-A')], [cyclicDef])
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not recurse infinitely on mutually cyclic subgraph definitions', () => {
|
||||
const defA = subgraphDef('def-A', [node(70, 'def-B')])
|
||||
const defB = subgraphDef('def-B', [node(80, 'def-A')])
|
||||
expect(() =>
|
||||
buildSubgraphExecutionPaths([node(5, 'def-A')], [defA, defB])
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { isSubgraphDefinition } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
|
||||
/**
|
||||
* Returns all ancestor execution IDs for a given execution ID, including itself.
|
||||
*
|
||||
* Example: "65:70:63" → ["65", "65:70", "65:70:63"]
|
||||
* @knipIgnoreUsedByStackedPR
|
||||
*/
|
||||
export function getAncestorExecutionIds(
|
||||
executionId: string | NodeExecutionId
|
||||
): NodeExecutionId[] {
|
||||
const parts = executionId.split(':')
|
||||
return Array.from({ length: parts.length }, (_, i) =>
|
||||
parts.slice(0, i + 1).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all ancestor execution IDs for a given execution ID, excluding itself.
|
||||
*
|
||||
* Example: "65:70:63" → ["65", "65:70"]
|
||||
* @knipIgnoreUsedByStackedPR
|
||||
*/
|
||||
export function getParentExecutionIds(
|
||||
executionId: string | NodeExecutionId
|
||||
): NodeExecutionId[] {
|
||||
return getAncestorExecutionIds(executionId).slice(0, -1)
|
||||
}
|
||||
|
||||
/**
|
||||
* "def-A" → ["5", "10"] for each container node instantiating that subgraph definition.
|
||||
* @knipIgnoreUsedByStackedPR
|
||||
*/
|
||||
export function buildSubgraphExecutionPaths(
|
||||
rootNodes: ComfyNode[],
|
||||
allSubgraphDefs: unknown[]
|
||||
): Map<string, string[]> {
|
||||
const subgraphDefMap = new Map(
|
||||
allSubgraphDefs.filter(isSubgraphDefinition).map((s) => [s.id, s])
|
||||
)
|
||||
const pathMap = new Map<string, string[]>()
|
||||
const visited = new Set<string>()
|
||||
|
||||
const build = (nodes: ComfyNode[], parentPrefix: string) => {
|
||||
for (const n of nodes ?? []) {
|
||||
if (typeof n.type !== 'string' || !subgraphDefMap.has(n.type)) continue
|
||||
const path = parentPrefix ? `${parentPrefix}:${n.id}` : String(n.id)
|
||||
const existing = pathMap.get(n.type)
|
||||
if (existing) {
|
||||
existing.push(path)
|
||||
} else {
|
||||
pathMap.set(n.type, [path])
|
||||
}
|
||||
|
||||
if (visited.has(n.type)) continue
|
||||
visited.add(n.type)
|
||||
|
||||
const innerDef = subgraphDefMap.get(n.type)
|
||||
if (innerDef) {
|
||||
build(innerDef.nodes, path)
|
||||
}
|
||||
|
||||
visited.delete(n.type)
|
||||
}
|
||||
}
|
||||
|
||||
build(rootNodes, '')
|
||||
return pathMap
|
||||
}
|
||||
@@ -165,7 +165,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { until, whenever } from '@vueuse/core'
|
||||
import { merge, stubTrue } from 'es-toolkit/compat'
|
||||
import type { AutoCompleteOptionSelectEvent } from 'primevue/autocomplete'
|
||||
import {
|
||||
@@ -211,8 +211,9 @@ import { useManagerState } from '@/workbench/extensions/manager/composables/useM
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { initialTab, onClose } = defineProps<{
|
||||
const { initialTab, initialPackId, onClose } = defineProps<{
|
||||
initialTab?: ManagerTab
|
||||
initialPackId?: string
|
||||
onClose: () => void
|
||||
}>()
|
||||
|
||||
@@ -347,8 +348,14 @@ const {
|
||||
sortOptions
|
||||
} = useRegistrySearch({
|
||||
initialSortField: initialState.sortField,
|
||||
initialSearchMode: initialState.searchMode,
|
||||
initialSearchQuery: initialState.searchQuery
|
||||
initialSearchMode:
|
||||
initialPackId && initialTabId !== ManagerTab.Missing
|
||||
? 'packs'
|
||||
: initialState.searchMode,
|
||||
initialSearchQuery:
|
||||
initialTabId === ManagerTab.Missing
|
||||
? ''
|
||||
: (initialPackId ?? initialState.searchQuery)
|
||||
})
|
||||
pageNumber.value = 0
|
||||
|
||||
@@ -475,6 +482,19 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Auto-select the pack matching initialPackId once
|
||||
if (initialPackId) {
|
||||
until(resultsWithKeys)
|
||||
.toMatch((packs) => packs.some((p) => p.id === initialPackId))
|
||||
.then((packs) => {
|
||||
const target = packs.find((p) => p.id === initialPackId)
|
||||
if (target && selectedNodePacks.value.length === 0) {
|
||||
selectedNodePacks.value = [target]
|
||||
isRightPanelOpen.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getLoadingCount = () => {
|
||||
switch (selectedTab.value?.id) {
|
||||
case ManagerTab.AllInstalled:
|
||||
|
||||
@@ -13,13 +13,14 @@ export function useManagerDialog() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
}
|
||||
|
||||
function show(initialTab?: ManagerTab) {
|
||||
function show(initialTab?: ManagerTab, initialPackId?: string) {
|
||||
dialogService.showLayoutDialog({
|
||||
key: DIALOG_KEY,
|
||||
component: ManagerDialog,
|
||||
props: {
|
||||
onClose: hide,
|
||||
initialTab
|
||||
initialTab,
|
||||
initialPackId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDi
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import { useManagerDialog } from '@/workbench/extensions/manager/composables/useManagerDialog'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
import type { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
export enum ManagerUIState {
|
||||
DISABLED = 'disabled',
|
||||
@@ -143,6 +143,7 @@ export function useManagerState() {
|
||||
*/
|
||||
const openManager = async (options?: {
|
||||
initialTab?: ManagerTab
|
||||
initialPackId?: string
|
||||
legacyCommand?: string
|
||||
showToastOnLegacyError?: boolean
|
||||
isLegacyOnly?: boolean
|
||||
@@ -181,16 +182,14 @@ export function useManagerState() {
|
||||
|
||||
case ManagerUIState.NEW_UI:
|
||||
if (options?.isLegacyOnly) {
|
||||
// Legacy command is not available in NEW_UI mode
|
||||
useToastStore().add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: t('manager.legacyMenuNotAvailable'),
|
||||
life: 3000
|
||||
})
|
||||
await managerDialog.show(ManagerTab.All)
|
||||
} else {
|
||||
await managerDialog.show(options?.initialTab)
|
||||
managerDialog.show(options?.initialTab, options?.initialPackId)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user