From e26e1f0c9efb77e2e461be2564691b1676f255f6 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Thu, 8 Jan 2026 16:49:56 -0800 Subject: [PATCH] feat: add HoneyToast component for persistent progress notifications (#7902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add HoneyToast, a persistent bottom-anchored notification component for long-running task progress, and migrate existing progress dialogs to use it. ## Changes - **What**: - New `HoneyToast` component with slot-based API, Teleport, transitions, and accessibility - Migrated `ModelImportProgressDialog` to use HoneyToast - Created `ManagerProgressToast` combining the old Header/Content/Footer components - Deleted deprecated `ManagerProgressDialogContent`, `ManagerProgressHeader`, `ManagerProgressFooter`, and `useManagerProgressDialogStore` - Removed no-op `showManagerProgressDialog`/`toggleManagerProgressDialog` functions - Added Storybook stories for HoneyToast and ProgressToastItem ## Review Focus - HoneyToast component design and slot API - ManagerProgressToast self-contained state management (auto-shows when `comfyManagerStore.taskLogs.length > 0`) - Accessibility attributes on the toast component ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7902-feat-add-HoneyToast-component-for-persistent-progress-notifications-2e26d73d365081c78ae6edc5accb326e) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: sno Co-authored-by: github-actions Co-authored-by: GitHub Action --- .storybook/main.ts | 2 +- AGENTS.md | 7 +- package.json | 1 + pnpm-lock.yaml | 134 ++++- pnpm-workspace.yaml | 1 + .../honeyToast/HoneyToast.stories.ts | 292 +++++++++++ src/components/honeyToast/HoneyToast.test.ts | 137 +++++ src/components/honeyToast/HoneyToast.vue | 46 ++ .../toast/ProgressToastItem.stories.ts | 94 ++++ src/components/toast/ProgressToastItem.vue | 8 +- src/composables/useCoreCommands.ts | 9 - .../components/ModelImportProgressDialog.vue | 319 ++++++------ src/services/dialogService.ts | 40 +- src/stores/README.md | 1 - src/views/GraphView.vue | 2 + .../ManagerProgressDialogContent.test.ts | 186 ------- .../ManagerProgressDialogContent.vue | 182 ------- .../components/ManagerProgressFooter.test.ts | 486 ------------------ .../components/ManagerProgressFooter.vue | 195 ------- .../components/ManagerProgressHeader.vue | 44 -- .../components/ManagerProgressToast.vue | 353 +++++++++++++ .../composables/useManagerQueue.test.ts | 7 - .../manager/composables/useManagerQueue.ts | 15 - .../manager/stores/comfyManagerStore.test.ts | 6 - .../manager/stores/comfyManagerStore.ts | 46 +- 25 files changed, 1220 insertions(+), 1393 deletions(-) create mode 100644 src/components/honeyToast/HoneyToast.stories.ts create mode 100644 src/components/honeyToast/HoneyToast.test.ts create mode 100644 src/components/honeyToast/HoneyToast.vue create mode 100644 src/components/toast/ProgressToastItem.stories.ts delete mode 100644 src/workbench/extensions/manager/components/ManagerProgressDialogContent.test.ts delete mode 100644 src/workbench/extensions/manager/components/ManagerProgressDialogContent.vue delete mode 100644 src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts delete mode 100644 src/workbench/extensions/manager/components/ManagerProgressFooter.vue delete mode 100644 src/workbench/extensions/manager/components/ManagerProgressHeader.vue create mode 100644 src/workbench/extensions/manager/components/ManagerProgressToast.vue diff --git a/.storybook/main.ts b/.storybook/main.ts index 897094ade..af23c6db3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -7,7 +7,7 @@ import type { InlineConfig } from 'vite' const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], - addons: ['@storybook/addon-docs'], + addons: ['@storybook/addon-docs', '@storybook/addon-mcp'], framework: { name: '@storybook/vue3-vite', options: {} diff --git a/AGENTS.md b/AGENTS.md index ca0985a7e..b68246d4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,7 +122,10 @@ The project uses **Nx** for build orchestration and task management - Prefer reactive props destructuring to `const props = defineProps<...>` - Do not use `withDefaults` or runtime props declaration - Do not import Vue macros unnecessarily - - Prefer `useModel` to separately defining a prop and emit + - Prefer `defineModel` to separately defining a prop and emit for v-model bindings + - Define slots via template usage, not `defineSlots` + - Use same-name shorthand for slot prop bindings: `:isExpanded` instead of `:is-expanded="isExpanded"` + - Derive component types using `vue-component-type-helpers` (`ComponentProps`, `ComponentSlots`) instead of separate type files - Be judicious with addition of new refs or other state - If it's possible to accomplish the design goals with just a prop, don't add a `ref` - If it's possible to use the `ref` or prop directly, don't add a `computed` @@ -271,6 +274,8 @@ When referencing Comfy-Org repos: - Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value - NEVER use `!important` or the `!` important prefix for tailwind classes - Find existing `!important` classes that are interfering with the styling and propose corrections of those instead. +- NEVER use arbitrary percentage values like `w-[80%]` when a Tailwind fraction utility exists + - Use `w-4/5` instead of `w-[80%]`, `w-1/2` instead of `w-[50%]`, etc. ## Agent-only rules diff --git a/package.json b/package.json index ecfb08da7..64eeb3cf5 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@prettier/plugin-oxc": "catalog:", "@sentry/vite-plugin": "catalog:", "@storybook/addon-docs": "catalog:", + "@storybook/addon-mcp": "catalog:", "@storybook/vue3": "catalog:", "@storybook/vue3-vite": "catalog:", "@tailwindcss/vite": "catalog:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6eea18564..b0fd3de72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ catalogs: '@storybook/addon-docs': specifier: ^10.1.9 version: 10.1.9 + '@storybook/addon-mcp': + specifier: 0.1.6 + version: 0.1.6 '@storybook/vue3': specifier: ^10.1.9 version: 10.1.9 @@ -549,6 +552,9 @@ importers: '@storybook/addon-docs': specifier: 'catalog:' version: 10.1.9(@types/react@19.1.9)(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)) + '@storybook/addon-mcp': + specifier: 'catalog:' + version: 0.1.6(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) '@storybook/vue3': specifier: 'catalog:' version: 10.1.9(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vue@3.5.13(typescript@5.9.3)) @@ -3148,6 +3154,11 @@ packages: peerDependencies: storybook: ^10.1.9 + '@storybook/addon-mcp@0.1.6': + resolution: {integrity: sha512-+EagCHqwIb9tg3DKskEsXpsqQVnMljxgR5Tt3Bu0ZpWweB1HdMy+ok128gzNfTZ3r+5ljksr0q66YCEkrQwdDA==} + peerDependencies: + storybook: ^9.1.16 || ^10.0.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 + '@storybook/builder-vite@10.1.9': resolution: {integrity: sha512-rUILpjGV7gKfXrUeZzpNAer9PspB3LJI1d+gJHISx2Gs24bdneA3y/gu0fWw46ccOSIcwb91xoK5QxliJcWsWg==} peerDependencies: @@ -3181,6 +3192,9 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@storybook/mcp@0.1.1': + resolution: {integrity: sha512-+AivFDms1XkY2VUvZBBYy0co5qvRh20eYXYwhaDPQXX2Q4y96arSkWn22e/l3DQwA9Ywzv481vj4gl4zPrCQkg==} + '@storybook/react-dom-shim@10.1.9': resolution: {integrity: sha512-gJsR6fI1gG4DSin6sQx8RmGDQF8Lije0cZbxHyVedNleBsveGXIPFUKFVi+pRNdwBPni1Z2g/gYyHzkOEqPD2w==} peerDependencies: @@ -3453,6 +3467,26 @@ packages: '@tiptap/starter-kit@2.10.4': resolution: {integrity: sha512-tu/WCs9Mkr5Nt8c3/uC4VvAbQlVX0OY7ygcqdzHGUeG9zP3twdW7o5xM3kyDKR2++sbVzqu5Ll5qNU+1JZvPGQ==} + '@tmcp/adapter-valibot@0.1.5': + resolution: {integrity: sha512-9P2wrVYPngemNK0UvPb/opC722/jfd09QxXmme1TRp/wPsl98vpSk/MXt24BCMqBRv4Dvs0xxJH4KHDcjXW52Q==} + peerDependencies: + tmcp: ^1.17.0 + valibot: ^1.1.0 + + '@tmcp/session-manager@0.2.1': + resolution: {integrity: sha512-DOGy9LfufXCy1wfpGHZ6qPSDQtRnTVwOb71+41ffovTqzLMZlK3iLK/LIsekHxIiku+iIAUiqEKN+DHbqEm8IA==} + peerDependencies: + tmcp: ^1.16.3 + + '@tmcp/transport-http@0.8.3': + resolution: {integrity: sha512-gnoBjDBd8/ppl4WRrNKPKHlioCxE8D0zTyNUOzqUjsg0s6GRsyB5iMirh9lC4QjQt0NEOrI+sIJdz+9ymf0MDA==} + peerDependencies: + '@tmcp/auth': ^0.3.3 || ^0.4.0 + tmcp: ^1.18.0 + peerDependenciesMeta: + '@tmcp/auth': + optional: true + '@trivago/prettier-plugin-sort-imports@5.2.2': resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} engines: {node: '>18.12'} @@ -3786,6 +3820,11 @@ packages: cpu: [x64] os: [win32] + '@valibot/to-json-schema@1.5.0': + resolution: {integrity: sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ==} + peerDependencies: + valibot: ^1.2.0 + '@vitejs/plugin-vue@6.0.3': resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5170,6 +5209,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + esm-resolve@1.0.11: resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==} @@ -5978,6 +6020,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-rpc-2.0@1.7.1: + resolution: {integrity: sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7386,6 +7431,9 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqids@0.3.0: + resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -7613,6 +7661,9 @@ packages: resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} hasBin: true + tmcp@1.19.0: + resolution: {integrity: sha512-wOY449EdaWDo7wLZEOVjeH9fn/AqfFF4f+3pDerCI8xHpy2Z8msUjAF0Vkg01aEFIdFMmiNDiY4hu6E7jVX79w==} + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -7858,6 +7909,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-template-matcher@1.1.2: + resolution: {integrity: sha512-uZc1h12jdO3m/R77SfTEOuo6VbMhgWznaawKpBjRGSJb7i91x5PgI37NQJtG+Cerxkk0yr1pylBY2qG1kQ+aEQ==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -7873,6 +7927,14 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -8019,6 +8081,9 @@ packages: vue-component-type-helpers@3.2.1: resolution: {integrity: sha512-gKV7XOkQl4urSuLHNY1tnVQf7wVgtb/mKbRyxSLWGZUY9RK7aDPhBenTjm+i8ZFe0zC2PZeHMPtOZXZfyaFOzQ==} + vue-component-type-helpers@3.2.2: + resolution: {integrity: sha512-x8C2nx5XlUNM0WirgfTkHjJGO/ABBxlANZDtHw2HclHtQnn+RFPTnbjMJn8jHZW4TlUam0asHcA14lf1C6Jb+A==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -10949,6 +11014,18 @@ snapshots: - vite - webpack + '@storybook/addon-mcp@0.1.6(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)': + dependencies: + '@storybook/mcp': 0.1.1(typescript@5.9.3) + '@tmcp/adapter-valibot': 0.1.5(tmcp@1.19.0(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3)) + '@tmcp/transport-http': 0.8.3(tmcp@1.19.0(typescript@5.9.3)) + storybook: 10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tmcp: 1.19.0(typescript@5.9.3) + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - '@tmcp/auth' + - typescript + '@storybook/builder-vite@10.1.9(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))': dependencies: '@storybook/csf-plugin': 10.1.9(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)) @@ -10978,6 +11055,16 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@storybook/mcp@0.1.1(typescript@5.9.3)': + dependencies: + '@tmcp/adapter-valibot': 0.1.5(tmcp@1.19.0(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3)) + '@tmcp/transport-http': 0.8.3(tmcp@1.19.0(typescript@5.9.3)) + tmcp: 1.19.0(typescript@5.9.3) + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - '@tmcp/auth' + - typescript + '@storybook/react-dom-shim@10.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: react: 19.2.3 @@ -11007,7 +11094,7 @@ snapshots: storybook: 10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.3) - vue-component-type-helpers: 3.2.1 + vue-component-type-helpers: 3.2.2 '@swc/helpers@0.5.17': dependencies: @@ -11275,6 +11362,23 @@ snapshots: '@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4)) '@tiptap/pm': 2.10.4 + '@tmcp/adapter-valibot@0.1.5(tmcp@1.19.0(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3))': + dependencies: + '@standard-schema/spec': 1.1.0 + '@valibot/to-json-schema': 1.5.0(valibot@1.2.0(typescript@5.9.3)) + tmcp: 1.19.0(typescript@5.9.3) + valibot: 1.2.0(typescript@5.9.3) + + '@tmcp/session-manager@0.2.1(tmcp@1.19.0(typescript@5.9.3))': + dependencies: + tmcp: 1.19.0(typescript@5.9.3) + + '@tmcp/transport-http@0.8.3(tmcp@1.19.0(typescript@5.9.3))': + dependencies: + '@tmcp/session-manager': 0.2.1(tmcp@1.19.0(typescript@5.9.3)) + esm-env: 1.2.2 + tmcp: 1.19.0(typescript@5.9.3) + '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)': dependencies: '@babel/generator': 7.28.5 @@ -11623,6 +11727,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@valibot/to-json-schema@1.5.0(valibot@1.2.0(typescript@5.9.3))': + dependencies: + valibot: 1.2.0(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.53 @@ -13303,6 +13411,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + esm-resolve@1.0.11: {} espree@10.4.0: @@ -14189,6 +14299,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-rpc-2.0@1.7.1: {} + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -16055,6 +16167,8 @@ snapshots: sprintf-js@1.0.3: {} + sqids@0.3.0: {} + stable-hash-x@0.2.0: {} stack-utils@2.0.6: @@ -16347,6 +16461,16 @@ snapshots: dependencies: tldts-core: 7.0.19 + tmcp@1.19.0(typescript@5.9.3): + dependencies: + '@standard-schema/spec': 1.1.0 + json-rpc-2.0: 1.7.1 + sqids: 0.3.0 + uri-template-matcher: 1.1.2 + valibot: 1.2.0(typescript@5.9.3) + transitivePeerDependencies: + - typescript + tmp@0.2.5: {} to-regex-range@5.0.1: @@ -16644,6 +16768,8 @@ snapshots: dependencies: punycode: 2.3.1 + uri-template-matcher@1.1.2: {} + use-sync-external-store@1.6.0(react@19.2.3): dependencies: react: 19.2.3 @@ -16654,6 +16780,10 @@ snapshots: uuid@11.1.0: {} + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -16914,6 +17044,8 @@ snapshots: vue-component-type-helpers@3.2.1: {} + vue-component-type-helpers@3.2.2: {} + vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)): dependencies: vue: 3.5.13(typescript@5.9.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bd4638b0d..56f3f394d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,6 +29,7 @@ catalog: '@sentry/vue': ^10.32.1 '@sparkjsdev/spark': ^0.1.10 '@storybook/addon-docs': ^10.1.9 + '@storybook/addon-mcp': 0.1.6 '@storybook/vue3': ^10.1.9 '@storybook/vue3-vite': ^10.1.9 '@tailwindcss/vite': ^4.1.12 diff --git a/src/components/honeyToast/HoneyToast.stories.ts b/src/components/honeyToast/HoneyToast.stories.ts new file mode 100644 index 000000000..98ae59070 --- /dev/null +++ b/src/components/honeyToast/HoneyToast.stories.ts @@ -0,0 +1,292 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import ProgressToastItem from '@/components/toast/ProgressToastItem.vue' +import Button from '@/components/ui/button/Button.vue' +import type { AssetDownload } from '@/stores/assetDownloadStore' +import { cn } from '@/utils/tailwindUtil' + +import HoneyToast from './HoneyToast.vue' + +function createMockJob(overrides: Partial = {}): AssetDownload { + return { + taskId: 'task-1', + assetId: 'asset-1', + assetName: 'model-v1.safetensors', + bytesTotal: 1000000, + bytesDownloaded: 0, + progress: 0, + status: 'created', + ...overrides + } +} + +const meta: Meta = { + title: 'Toast/HoneyToast', + component: HoneyToast, + parameters: { + layout: 'fullscreen' + }, + decorators: [ + () => ({ + template: '
' + }) + ] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(false) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + status: 'completed', + progress: 1 + }), + createMockJob({ + taskId: 'task-2', + assetName: 'lora-style.safetensors', + status: 'running', + progress: 0.45 + }), + createMockJob({ + taskId: 'task-3', + assetName: 'vae-decoder.safetensors', + status: 'created' + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const Expanded: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(true) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + status: 'completed', + progress: 1 + }), + createMockJob({ + taskId: 'task-2', + assetName: 'lora-style.safetensors', + status: 'running', + progress: 0.45 + }), + createMockJob({ + taskId: 'task-3', + assetName: 'vae-decoder.safetensors', + status: 'created' + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const Completed: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(false) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + bytesDownloaded: 1000000, + progress: 1, + status: 'completed' + }), + createMockJob({ + taskId: 'task-2', + assetId: 'asset-2', + assetName: 'lora-style.safetensors', + bytesTotal: 500000, + bytesDownloaded: 500000, + progress: 1, + status: 'completed' + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const WithError: Story = { + render: () => ({ + components: { HoneyToast, Button, ProgressToastItem }, + setup() { + const isExpanded = ref(true) + const jobs = [ + createMockJob({ + taskId: 'task-1', + assetName: 'model-v1.safetensors', + status: 'failed', + progress: 0.23 + }), + createMockJob({ + taskId: 'task-2', + assetName: 'lora-style.safetensors', + status: 'completed', + progress: 1 + }) + ] + return { isExpanded, cn, jobs } + }, + template: ` + + + + + + ` + }) +} + +export const Hidden: Story = { + render: () => ({ + components: { HoneyToast }, + template: ` +
+

HoneyToast is hidden when visible=false. Nothing appears at the bottom.

+ + + + + +
+ ` + }) +} diff --git a/src/components/honeyToast/HoneyToast.test.ts b/src/components/honeyToast/HoneyToast.test.ts new file mode 100644 index 000000000..ada123053 --- /dev/null +++ b/src/components/honeyToast/HoneyToast.test.ts @@ -0,0 +1,137 @@ +import type { VueWrapper } from '@vue/test-utils' +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { defineComponent, h, nextTick, ref } from 'vue' + +import HoneyToast from './HoneyToast.vue' + +describe('HoneyToast', () => { + beforeEach(() => { + vi.clearAllMocks() + document.body.innerHTML = '' + }) + + function mountComponent( + props: { visible: boolean; expanded?: boolean } = { visible: true } + ): VueWrapper { + return mount(HoneyToast, { + props, + slots: { + default: (slotProps: { isExpanded: boolean }) => + h( + 'div', + { 'data-testid': 'content' }, + slotProps.isExpanded ? 'expanded' : 'collapsed' + ), + footer: (slotProps: { isExpanded: boolean; toggle: () => void }) => + h( + 'button', + { + 'data-testid': 'toggle-btn', + onClick: slotProps.toggle + }, + slotProps.isExpanded ? 'Collapse' : 'Expand' + ) + }, + attachTo: document.body + }) + } + + it('renders when visible is true', async () => { + const wrapper = mountComponent({ visible: true }) + await nextTick() + + const toast = document.body.querySelector('[role="status"]') + expect(toast).toBeTruthy() + + wrapper.unmount() + }) + + it('does not render when visible is false', async () => { + const wrapper = mountComponent({ visible: false }) + await nextTick() + + const toast = document.body.querySelector('[role="status"]') + expect(toast).toBeFalsy() + + wrapper.unmount() + }) + + it('passes is-expanded=false to slots by default', async () => { + const wrapper = mountComponent({ visible: true }) + await nextTick() + + const content = document.body.querySelector('[data-testid="content"]') + expect(content?.textContent).toBe('collapsed') + + wrapper.unmount() + }) + + it('applies collapsed max-height class when collapsed', async () => { + const wrapper = mountComponent({ visible: true, expanded: false }) + await nextTick() + + const expandableArea = document.body.querySelector( + '[role="status"] > div:first-child' + ) + expect(expandableArea?.classList.contains('max-h-0')).toBe(true) + + wrapper.unmount() + }) + + it('has aria-live="polite" for accessibility', async () => { + const wrapper = mountComponent({ visible: true }) + await nextTick() + + const toast = document.body.querySelector('[role="status"]') + expect(toast?.getAttribute('aria-live')).toBe('polite') + + wrapper.unmount() + }) + + it('supports v-model:expanded with reactive parent state', async () => { + const TestWrapper = defineComponent({ + components: { HoneyToast }, + setup() { + const expanded = ref(false) + return { expanded } + }, + template: ` + + + + + ` + }) + + const wrapper = mount(TestWrapper, { attachTo: document.body }) + await nextTick() + + const content = document.body.querySelector('[data-testid="content"]') + expect(content?.textContent).toBe('collapsed') + + const toggleBtn = document.body.querySelector( + '[data-testid="toggle-btn"]' + ) as HTMLButtonElement + expect(toggleBtn?.textContent?.trim()).toBe('Expand') + + toggleBtn?.click() + await nextTick() + + expect(content?.textContent).toBe('expanded') + expect(toggleBtn?.textContent?.trim()).toBe('Collapse') + + const expandableArea = document.body.querySelector( + '[role="status"] > div:first-child' + ) + expect(expandableArea?.classList.contains('max-h-[400px]')).toBe(true) + + wrapper.unmount() + }) +}) diff --git a/src/components/honeyToast/HoneyToast.vue b/src/components/honeyToast/HoneyToast.vue new file mode 100644 index 000000000..4da85ce95 --- /dev/null +++ b/src/components/honeyToast/HoneyToast.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/components/toast/ProgressToastItem.stories.ts b/src/components/toast/ProgressToastItem.stories.ts new file mode 100644 index 000000000..2ad376a72 --- /dev/null +++ b/src/components/toast/ProgressToastItem.stories.ts @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import type { AssetDownload } from '@/stores/assetDownloadStore' + +import ProgressToastItem from './ProgressToastItem.vue' + +const meta: Meta = { + title: 'Toast/ProgressToastItem', + component: ProgressToastItem, + parameters: { + layout: 'padded' + }, + decorators: [ + () => ({ + template: '
' + }) + ] +} + +export default meta +type Story = StoryObj + +function createMockJob(overrides: Partial = {}): AssetDownload { + return { + taskId: 'task-1', + assetId: 'asset-1', + assetName: 'model-v1.safetensors', + bytesTotal: 1000000, + bytesDownloaded: 0, + progress: 0, + status: 'created', + ...overrides + } +} + +export const Pending: Story = { + args: { + job: createMockJob({ + status: 'created', + assetName: 'sd-xl-base-1.0.safetensors' + }) + } +} + +export const Running: Story = { + args: { + job: createMockJob({ + status: 'running', + progress: 0.45, + assetName: 'lora-detail-enhancer.safetensors' + }) + } +} + +export const RunningAlmostComplete: Story = { + args: { + job: createMockJob({ + status: 'running', + progress: 0.92, + assetName: 'vae-ft-mse-840000.safetensors' + }) + } +} + +export const Completed: Story = { + args: { + job: createMockJob({ + status: 'completed', + progress: 1, + assetName: 'controlnet-canny.safetensors' + }) + } +} + +export const Failed: Story = { + args: { + job: createMockJob({ + status: 'failed', + progress: 0.23, + assetName: 'unreachable-model.safetensors' + }) + } +} + +export const LongFileName: Story = { + args: { + job: createMockJob({ + status: 'running', + progress: 0.67, + assetName: + 'very-long-model-name-with-lots-of-descriptive-text-v2.1-final-release.safetensors' + }) + } +} diff --git a/src/components/toast/ProgressToastItem.vue b/src/components/toast/ProgressToastItem.vue index 495af2e24..80869d8f9 100644 --- a/src/components/toast/ProgressToastItem.vue +++ b/src/components/toast/ProgressToastItem.vue @@ -30,9 +30,7 @@ const isPending = computed(() => job.status === 'created') >
{{ job.assetName }} - - {{ progressPercent }}% - +
@@ -49,9 +47,9 @@ const isPending = computed(() => job.status === 'created') diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 6b2aa63c2..032f1f57b 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -905,15 +905,6 @@ export function useCoreCommands(): ComfyCommand[] { }) } }, - { - id: 'Comfy.Manager.ToggleManagerProgressDialog', - icon: 'pi pi-spinner', - label: 'Toggle the Custom Nodes Manager Progress Bar', - versionAdded: '1.13.9', - function: () => { - dialogService.toggleManagerProgressDialog() - } - }, { id: 'Comfy.User.OpenSignInDialog', icon: 'pi pi-user', diff --git a/src/platform/assets/components/ModelImportProgressDialog.vue b/src/platform/assets/components/ModelImportProgressDialog.vue index d5f8c8666..b9191526f 100644 --- a/src/platform/assets/components/ModelImportProgressDialog.vue +++ b/src/platform/assets/components/ModelImportProgressDialog.vue @@ -1,8 +1,10 @@ diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index d2d337ced..5bbcaf237 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -25,9 +25,7 @@ import type { DialogComponentProps, ShowDialogOptions } from '@/stores/dialogStore' -import ManagerProgressDialogContent from '@/workbench/extensions/manager/components/ManagerProgressDialogContent.vue' -import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue' -import ManagerProgressHeader from '@/workbench/extensions/manager/components/ManagerProgressHeader.vue' + import ManagerDialogContent from '@/workbench/extensions/manager/components/manager/ManagerDialogContent.vue' import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue' import ImportFailedNodeContent from '@/workbench/extensions/manager/components/manager/ImportFailedNodeContent.vue' @@ -232,30 +230,6 @@ export const useDialogService = () => { }) } - function showManagerProgressDialog(options?: { - props?: InstanceType['$props'] - }) { - return dialogStore.showDialog({ - key: 'global-manager-progress-dialog', - component: ManagerProgressDialogContent, - headerComponent: ManagerProgressHeader, - footerComponent: ManagerProgressFooter, - props: options?.props, - priority: 2, - dialogComponentProps: { - closable: false, - modal: false, - position: 'bottom', - pt: { - root: { class: 'w-[80%] max-w-2xl mx-auto border-none' }, - content: { class: 'p-0!' }, - header: { class: 'p-0! border-none' }, - footer: { class: 'p-0! border-none' } - } - } - }) - } - /** * Shows a dialog requiring sign in for API nodes * @returns Promise that resolves to true if user clicks login, false if cancelled @@ -443,16 +417,6 @@ export const useDialogService = () => { } } - function toggleManagerProgressDialog( - props?: ComponentAttrs - ) { - if (dialogStore.isDialogOpen('global-manager-progress-dialog')) { - dialogStore.closeDialog({ key: 'global-manager-progress-dialog' }) - } else { - showManagerProgressDialog({ props }) - } - } - function showLayoutDialog(options: { key: string component: Component @@ -588,7 +552,6 @@ export const useDialogService = () => { showAboutDialog, showExecutionErrorDialog, showManagerDialog, - showManagerProgressDialog, showApiNodesSignInDialog, showSignInDialog, showSubscriptionRequiredDialog, @@ -599,7 +562,6 @@ export const useDialogService = () => { showErrorDialog, confirm, toggleManagerDialog, - toggleManagerProgressDialog, showLayoutDialog, showImportFailedNodeDialog, showNodeConflictDialog diff --git a/src/stores/README.md b/src/stores/README.md index a24a106e5..a3cc0ae5f 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -109,7 +109,6 @@ The following table lists ALL 46 store instances in the system as of 2025-09-01: | aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI | | apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth | | comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core | -| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI | | comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry | | commandStore.ts | useCommandStore | Manages commands and command execution | Core | | dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI | diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index e95257083..e60df5692 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -18,6 +18,7 @@ + @@ -80,6 +81,7 @@ import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' import { useWorkspaceStore } from '@/stores/workspaceStore' import { electronAPI, isElectron } from '@/utils/envUtil' import LinearView from '@/views/LinearView.vue' +import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue' setupAutoQueueHandler() useProgressFavicon() diff --git a/src/workbench/extensions/manager/components/ManagerProgressDialogContent.test.ts b/src/workbench/extensions/manager/components/ManagerProgressDialogContent.test.ts deleted file mode 100644 index b53e0fbec..000000000 --- a/src/workbench/extensions/manager/components/ManagerProgressDialogContent.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { VueWrapper } from '@vue/test-utils' -import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' -import Button from '@/components/ui/button/Button.vue' -import PrimeVue from 'primevue/config' -import Panel from 'primevue/panel' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' -import { createI18n } from 'vue-i18n' - -import enMessages from '@/locales/en/main.json' with { type: 'json' } - -import ManagerProgressDialogContent from './ManagerProgressDialogContent.vue' - -type ComponentInstance = InstanceType & { - lastPanelRef: HTMLElement | null - onLogsAdded: () => void - handleScroll: (e: { target: HTMLElement }) => void - isUserScrolling: boolean - resetUserScrolling: () => void - collapsedPanels: Record - togglePanel: (index: number) => void -} - -const mockCollapse = vi.fn() - -const defaultMockTaskLogs = [ - { taskName: 'Task 1', logs: ['Log 1', 'Log 2'] }, - { taskName: 'Task 2', logs: ['Log 3', 'Log 4'] } -] - -vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({ - useComfyManagerStore: vi.fn(() => ({ - taskLogs: [...defaultMockTaskLogs], - succeededTasksLogs: [...defaultMockTaskLogs], - failedTasksLogs: [...defaultMockTaskLogs], - managerQueue: { historyCount: 2 }, - isLoading: false - })), - useManagerProgressDialogStore: vi.fn(() => ({ - isExpanded: true, - activeTabIndex: 0, - getActiveTabIndex: vi.fn(() => 0), - setActiveTabIndex: vi.fn(), - toggle: vi.fn(), - collapse: mockCollapse, - expand: vi.fn() - })) -})) - -describe('ManagerProgressDialogContent', () => { - beforeEach(() => { - vi.clearAllMocks() - mockCollapse.mockReset() - }) - - const mountComponent = ({ - props = {} - }: Record = {}): VueWrapper => { - const i18n = createI18n({ - legacy: false, - locale: 'en', - messages: { en: enMessages } - }) - - return mount(ManagerProgressDialogContent, { - props: { - ...props - }, - global: { - plugins: [PrimeVue, createPinia(), i18n], - components: { - Panel, - Button - } - } - }) as VueWrapper - } - - it('renders the correct number of panels', async () => { - const wrapper = mountComponent() - await nextTick() - expect(wrapper.findAllComponents(Panel).length).toBe(2) - }) - - it('expands the last panel by default', async () => { - const wrapper = mountComponent() - await nextTick() - expect(wrapper.vm.collapsedPanels[1]).toBeFalsy() - }) - - it('toggles panel expansion when toggle method is called', async () => { - const wrapper = mountComponent() - await nextTick() - - // Initial state - first panel should be collapsed - expect(wrapper.vm.collapsedPanels[0]).toBeFalsy() - - wrapper.vm.togglePanel(0) - await nextTick() - - // After toggle - first panel should be expanded - expect(wrapper.vm.collapsedPanels[0]).toBe(true) - - wrapper.vm.togglePanel(0) - await nextTick() - - expect(wrapper.vm.collapsedPanels[0]).toBeFalsy() - }) - - it('displays the correct status for each panel', async () => { - const wrapper = mountComponent() - await nextTick() - - // Expand all panels to see status text - const panels = wrapper.findAllComponents(Panel) - for (let i = 0; i < panels.length; i++) { - if (!wrapper.vm.collapsedPanels[i]) { - wrapper.vm.togglePanel(i) - await nextTick() - } - } - - const panelsText = wrapper - .findAllComponents(Panel) - .map((panel) => panel.text()) - - expect(panelsText[0]).toContain('Completed ✓') - expect(panelsText[1]).toContain('Completed ✓') - }) - - it('auto-scrolls to bottom when new logs are added', async () => { - const wrapper = mountComponent() - await nextTick() - - const mockScrollElement = document.createElement('div') - Object.defineProperty(mockScrollElement, 'scrollHeight', { value: 200 }) - Object.defineProperty(mockScrollElement, 'clientHeight', { value: 100 }) - Object.defineProperty(mockScrollElement, 'scrollTop', { - value: 0, - writable: true - }) - - wrapper.vm.lastPanelRef = mockScrollElement - - wrapper.vm.onLogsAdded() - await nextTick() - - // Check if scrollTop is set to scrollHeight (scrolled to bottom) - expect(mockScrollElement.scrollTop).toBe(200) - }) - - it('does not auto-scroll when user is manually scrolling', async () => { - const wrapper = mountComponent() - await nextTick() - - const mockScrollElement = document.createElement('div') - Object.defineProperty(mockScrollElement, 'scrollHeight', { value: 200 }) - Object.defineProperty(mockScrollElement, 'clientHeight', { value: 100 }) - Object.defineProperty(mockScrollElement, 'scrollTop', { - value: 50, - writable: true - }) - - wrapper.vm.lastPanelRef = mockScrollElement - - wrapper.vm.handleScroll({ target: mockScrollElement }) - await nextTick() - - expect(wrapper.vm.isUserScrolling).toBe(true) - - // Now trigger the log update - wrapper.vm.onLogsAdded() - await nextTick() - - // Check that scrollTop is not changed (should still be 50) - expect(mockScrollElement.scrollTop).toBe(50) - }) - - it('calls collapse method when component is unmounted', async () => { - const wrapper = mountComponent() - await nextTick() - wrapper.unmount() - expect(mockCollapse).toHaveBeenCalled() - }) -}) diff --git a/src/workbench/extensions/manager/components/ManagerProgressDialogContent.vue b/src/workbench/extensions/manager/components/ManagerProgressDialogContent.vue deleted file mode 100644 index 14280dfad..000000000 --- a/src/workbench/extensions/manager/components/ManagerProgressDialogContent.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - diff --git a/src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts b/src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts deleted file mode 100644 index 024e025b8..000000000 --- a/src/workbench/extensions/manager/components/ManagerProgressFooter.test.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' -import PrimeVue from 'primevue/config' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { nextTick } from 'vue' -import { createI18n } from 'vue-i18n' - -import { useSettingStore } from '@/platform/settings/settingStore' -import { useCommandStore } from '@/stores/commandStore' -import { useDialogStore } from '@/stores/dialogStore' -import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue' -import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' -import { - useComfyManagerStore, - useManagerProgressDialogStore -} from '@/workbench/extensions/manager/stores/comfyManagerStore' -import type { TaskLog } from '@/workbench/extensions/manager/types/comfyManagerTypes' - -// Mock modules -vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore') -vi.mock('@/stores/dialogStore') -vi.mock('@/platform/settings/settingStore') -vi.mock('@/stores/commandStore') -vi.mock('@/workbench/extensions/manager/services/comfyManagerService') -vi.mock( - '@/workbench/extensions/manager/composables/useConflictDetection', - () => ({ - useConflictDetection: vi.fn(() => ({ - conflictedPackages: { value: [] }, - runFullConflictAnalysis: vi.fn().mockResolvedValue(undefined) - })) - }) -) - -// Mock useEventListener to capture the event handler -let reconnectHandler: (() => void) | null = null -vi.mock('@vueuse/core', async () => { - const actual = await vi.importActual('@vueuse/core') - return { - ...actual, - useEventListener: vi.fn( - (_target: any, event: string, handler: any, _options: any) => { - if (event === 'reconnected') { - reconnectHandler = handler - } - } - ) - } -}) -vi.mock('@/platform/workflow/core/services/workflowService', () => ({ - useWorkflowService: vi.fn(() => ({ - reloadCurrentWorkflow: vi.fn().mockResolvedValue(undefined) - })) -})) -vi.mock('@/stores/workspace/colorPaletteStore', () => ({ - useColorPaletteStore: vi.fn(() => ({ - completedActivePalette: { - light_theme: false - } - })) -})) - -// Helper function to mount component with required setup -const mountComponent = (options: { captureError?: boolean } = {}) => { - const pinia = createPinia() - setActivePinia(pinia) - - const i18n = createI18n({ - legacy: false, - locale: 'en', - messages: { - en: { - g: { - close: 'Close', - progressCountOf: 'of' - }, - contextMenu: { - Collapse: 'Collapse', - Expand: 'Expand' - }, - manager: { - clickToFinishSetup: 'Click', - applyChanges: 'Apply Changes', - toFinishSetup: 'to finish setup', - restartingBackend: 'Restarting backend to apply changes...', - extensionsSuccessfullyInstalled: - 'Extension(s) successfully installed and are ready to use!', - restartToApplyChanges: 'To apply changes, please restart ComfyUI', - installingDependencies: 'Installing dependencies...' - } - } - } - }) - - const config: any = { - global: { - plugins: [pinia, PrimeVue, i18n] - } - } - - // Add error handler for tests that expect errors - if (options.captureError) { - config.global.config = { - errorHandler: () => { - // Suppress error in test - } - } - } - - return mount(ManagerProgressFooter, config) -} - -describe('ManagerProgressFooter', () => { - const mockTaskLogs: TaskLog[] = [] - - const mockComfyManagerStore = { - taskLogs: mockTaskLogs, - allTasksDone: true, - isProcessingTasks: false, - succeededTasksIds: [] as string[], - failedTasksIds: [] as string[], - taskHistory: {} as Record, - taskQueue: null, - resetTaskState: vi.fn(), - clearLogs: vi.fn(), - setStale: vi.fn(), - // Add other required properties - isLoading: { value: false }, - error: { value: null }, - statusMessage: { value: 'DONE' }, - installedPacks: {}, - installedPacksIds: new Set(), - isPackInstalled: vi.fn(), - isPackEnabled: vi.fn(), - getInstalledPackVersion: vi.fn(), - refreshInstalledList: vi.fn(), - installPack: vi.fn(), - uninstallPack: vi.fn(), - updatePack: vi.fn(), - updateAllPacks: vi.fn(), - disablePack: vi.fn(), - enablePack: vi.fn() - } - - const mockDialogStore = { - closeDialog: vi.fn(), - // Add other required properties - dialogStack: { value: [] }, - showDialog: vi.fn(), - $id: 'dialog', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $dispose: vi.fn(), - $onAction: vi.fn() - } - - const mockSettingStore = { - get: vi.fn().mockReturnValue(false), - set: vi.fn(), - // Add other required properties - settingValues: { value: {} }, - settingsById: { value: {} }, - exists: vi.fn(), - getDefaultValue: vi.fn(), - loadSettingValues: vi.fn(), - updateValue: vi.fn(), - $id: 'setting', - $state: {} as any, - $patch: vi.fn(), - $reset: vi.fn(), - $subscribe: vi.fn(), - $dispose: vi.fn(), - $onAction: vi.fn() - } - - const mockProgressDialogStore = { - isExpanded: false, - toggle: vi.fn(), - collapse: vi.fn(), - expand: vi.fn() - } - - const mockCommandStore = { - execute: vi.fn().mockResolvedValue(undefined) - } - - const mockComfyManagerService = { - rebootComfyUI: vi.fn().mockResolvedValue(null) - } - - beforeEach(() => { - vi.clearAllMocks() - // Create new pinia instance for each test - const pinia = createPinia() - setActivePinia(pinia) - - // Reset task logs - mockTaskLogs.length = 0 - mockComfyManagerStore.taskLogs = mockTaskLogs - // Reset event handler - reconnectHandler = null - - vi.mocked(useComfyManagerStore).mockReturnValue( - mockComfyManagerStore as any - ) - vi.mocked(useDialogStore).mockReturnValue(mockDialogStore as any) - vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any) - vi.mocked(useManagerProgressDialogStore).mockReturnValue( - mockProgressDialogStore as any - ) - vi.mocked(useCommandStore).mockReturnValue(mockCommandStore as any) - vi.mocked(useComfyManagerService).mockReturnValue( - mockComfyManagerService as any - ) - }) - - describe('State 1: Queue Running', () => { - it('should display loading spinner and progress counter when queue is running', async () => { - // Setup queue running state - mockComfyManagerStore.isProcessingTasks = true - mockComfyManagerStore.succeededTasksIds = ['1', '2'] - mockComfyManagerStore.failedTasksIds = [] - mockComfyManagerStore.taskHistory = { - '1': { taskName: 'Installing pack1' }, - '2': { taskName: 'Installing pack2' }, - '3': { taskName: 'Installing pack3' } - } - mockTaskLogs.push( - { taskName: 'Installing pack1', taskId: '1', logs: [] }, - { taskName: 'Installing pack2', taskId: '2', logs: [] }, - { taskName: 'Installing pack3', taskId: '3', logs: [] } - ) - - const wrapper = mountComponent() - - // Check loading spinner exists (DotSpinner component) - expect(wrapper.find('.inline-flex').exists()).toBe(true) - - // Check current task name is displayed - expect(wrapper.text()).toContain('Installing pack3') - - // Check progress counter (completed: 2 of 3) - expect(wrapper.text()).toMatch(/2.*of.*3/) - - // Check expand/collapse button exists - const expandButton = wrapper.find('[aria-label="Expand"]') - expect(expandButton.exists()).toBe(true) - - // Check Apply Changes button is NOT shown - expect(wrapper.text()).not.toContain('Apply Changes') - }) - - it('should toggle expansion when expand button is clicked', async () => { - mockComfyManagerStore.isProcessingTasks = true - mockTaskLogs.push({ taskName: 'Installing', taskId: '1', logs: [] }) - - const wrapper = mountComponent() - - const expandButton = wrapper.find('[aria-label="Expand"]') - await expandButton.trigger('click') - - expect(mockProgressDialogStore.toggle).toHaveBeenCalled() - }) - }) - - describe('State 2: Tasks Completed (Waiting for Restart)', () => { - it('should display check mark and Apply Changes button when all tasks are done', async () => { - // Setup tasks completed state - mockComfyManagerStore.isProcessingTasks = false - mockTaskLogs.push( - { taskName: 'Installed pack1', taskId: '1', logs: [] }, - { taskName: 'Installed pack2', taskId: '2', logs: [] } - ) - mockComfyManagerStore.allTasksDone = true - - const wrapper = mountComponent() - - // Check check mark emoji - expect(wrapper.text()).toContain('✅') - - // Check restart message - expect(wrapper.text()).toContain( - 'To apply changes, please restart ComfyUI' - ) - expect(wrapper.text()).toContain('Apply Changes') - - // Check Apply Changes button exists - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - expect(applyButton).toBeTruthy() - - // Check no progress counter - expect(wrapper.text()).not.toMatch(/\d+.*of.*\d+/) - }) - }) - - describe('State 3: Restarting', () => { - it('should display restarting message and spinner during restart', async () => { - // Setup completed state first - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - - const wrapper = mountComponent() - - // Click Apply Changes to trigger restart - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Wait for state update - await nextTick() - - // Check restarting message - expect(wrapper.text()).toContain('Restarting backend to apply changes...') - - // Check loading spinner during restart - expect(wrapper.find('.inline-flex').exists()).toBe(true) - - // Check Apply Changes button is hidden - expect(wrapper.text()).not.toContain('Apply Changes') - }) - }) - - describe('State 4: Restart Completed', () => { - it('should display success message and auto-close after 3 seconds', async () => { - vi.useFakeTimers() - - // Setup completed state - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - - const wrapper = mountComponent() - - // Trigger restart - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Wait for event listener to be set up - await nextTick() - - // Trigger the reconnect handler directly - if (reconnectHandler) { - await reconnectHandler() - } - - // Wait for restart completed state - await nextTick() - - // Check success message - expect(wrapper.text()).toContain('🎉') - expect(wrapper.text()).toContain( - 'Extension(s) successfully installed and are ready to use!' - ) - - // Check dialog closes after 3 seconds - vi.advanceTimersByTime(3000) - - await nextTick() - - expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({ - key: 'global-manager-progress-dialog' - }) - expect(mockComfyManagerStore.resetTaskState).toHaveBeenCalled() - - vi.useRealTimers() - }) - }) - - describe('Common Features', () => { - it('should always display close button', async () => { - const wrapper = mountComponent() - - const closeButton = wrapper.find('[aria-label="Close"]') - expect(closeButton.exists()).toBe(true) - }) - - it('should close dialog when close button is clicked', async () => { - const wrapper = mountComponent() - - const closeButton = wrapper.find('[aria-label="Close"]') - await closeButton.trigger('click') - - expect(mockDialogStore.closeDialog).toHaveBeenCalledWith({ - key: 'global-manager-progress-dialog' - }) - }) - }) - - describe('Toast Management', () => { - it('should suppress reconnection toasts during restart', async () => { - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - mockSettingStore.get.mockReturnValue(false) // Original setting - - const wrapper = mountComponent() - - // Click Apply Changes - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Check toast setting was disabled - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.Toast.DisableReconnectingToast', - true - ) - }) - - it('should restore toast settings after restart completes', async () => { - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - mockSettingStore.get.mockReturnValue(false) // Original setting - - const wrapper = mountComponent() - - // Click Apply Changes - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - await applyButton?.trigger('click') - - // Wait for event listener to be set up - await nextTick() - - // Trigger the reconnect handler directly - if (reconnectHandler) { - await reconnectHandler() - } - - // Wait for settings restoration - await nextTick() - - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.Toast.DisableReconnectingToast', - false // Restored to original - ) - }) - }) - - describe('Error Handling', () => { - it('should restore state and close dialog on restart error', async () => { - mockComfyManagerStore.isProcessingTasks = false - mockComfyManagerStore.allTasksDone = true - - // Mock restart to throw error - mockComfyManagerService.rebootComfyUI.mockRejectedValue( - new Error('Restart failed') - ) - - const wrapper = mountComponent({ captureError: true }) - - // Click Apply Changes - const applyButton = wrapper - .findAll('button') - .find((btn) => btn.text().includes('Apply Changes')) - - expect(applyButton).toBeTruthy() - - // The component throws the error but Vue Test Utils catches it - // We need to check if the error handling logic was executed - await applyButton!.trigger('click').catch(() => { - // Error is expected, ignore it - }) - - // Wait for error handling - await nextTick() - - // Check dialog was closed on error - expect(mockDialogStore.closeDialog).toHaveBeenCalled() - // Check toast settings were restored - expect(mockSettingStore.set).toHaveBeenCalledWith( - 'Comfy.Toast.DisableReconnectingToast', - false - ) - // Check that the error handler was called - expect(mockComfyManagerService.rebootComfyUI).toHaveBeenCalled() - }) - }) -}) diff --git a/src/workbench/extensions/manager/components/ManagerProgressFooter.vue b/src/workbench/extensions/manager/components/ManagerProgressFooter.vue deleted file mode 100644 index f8fe0052d..000000000 --- a/src/workbench/extensions/manager/components/ManagerProgressFooter.vue +++ /dev/null @@ -1,195 +0,0 @@ - - - diff --git a/src/workbench/extensions/manager/components/ManagerProgressHeader.vue b/src/workbench/extensions/manager/components/ManagerProgressHeader.vue deleted file mode 100644 index 686d43436..000000000 --- a/src/workbench/extensions/manager/components/ManagerProgressHeader.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/src/workbench/extensions/manager/components/ManagerProgressToast.vue b/src/workbench/extensions/manager/components/ManagerProgressToast.vue new file mode 100644 index 000000000..3e6305b7e --- /dev/null +++ b/src/workbench/extensions/manager/components/ManagerProgressToast.vue @@ -0,0 +1,353 @@ + + + diff --git a/src/workbench/extensions/manager/composables/useManagerQueue.test.ts b/src/workbench/extensions/manager/composables/useManagerQueue.test.ts index 502867b8b..740a1e5cf 100644 --- a/src/workbench/extensions/manager/composables/useManagerQueue.test.ts +++ b/src/workbench/extensions/manager/composables/useManagerQueue.test.ts @@ -4,13 +4,6 @@ import { ref } from 'vue' import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue' import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes' -// Mock dialog service -vi.mock('@/services/dialogService', () => ({ - useDialogService: () => ({ - showManagerProgressDialog: vi.fn() - }) -})) - // Mock the app API vi.mock('@/scripts/app', () => ({ app: { diff --git a/src/workbench/extensions/manager/composables/useManagerQueue.ts b/src/workbench/extensions/manager/composables/useManagerQueue.ts index 3886b40bf..7ac796414 100644 --- a/src/workbench/extensions/manager/composables/useManagerQueue.ts +++ b/src/workbench/extensions/manager/composables/useManagerQueue.ts @@ -4,7 +4,6 @@ import type { Ref } from 'vue' import { computed, ref } from 'vue' import { app } from '@/scripts/app' -import { useDialogService } from '@/services/dialogService' import { normalizePackKeys } from '@/utils/packUtils' import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes' @@ -27,8 +26,6 @@ export const useManagerQueue = ( taskQueue: Ref, installedPacks: Ref> ) => { - const { showManagerProgressDialog } = useDialogService() - // Task queue state (read-only from server) const maxHistoryItems = ref(64) const isLoading = ref(false) @@ -113,15 +110,6 @@ export const useManagerQueue = ( (event: CustomEvent) => { if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) { updateTaskState(event.detail.state) - - // If no more tasks are running/pending, hide the progress dialog - if (allTasksDone.value) { - setTimeout(() => { - if (allTasksDone.value) { - showManagerProgressDialog() - } - }, 1000) // Small delay to let users see completion - } } } ) @@ -133,9 +121,6 @@ export const useManagerQueue = ( (event: CustomEvent) => { if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) { updateTaskState(event.detail.state) - - // Show progress dialog when a task starts - showManagerProgressDialog() } } ) diff --git a/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts index d131a0fc9..d1754d59a 100644 --- a/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts +++ b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts @@ -17,12 +17,6 @@ vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({ useComfyManagerService: vi.fn() })) -vi.mock('@/services/dialogService', () => ({ - useDialogService: () => ({ - showManagerProgressDialog: vi.fn() - }) -})) - vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => { const enqueueTaskMock = vi.fn() diff --git a/src/workbench/extensions/manager/stores/comfyManagerStore.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.ts index 06097cbe7..df6df758a 100644 --- a/src/workbench/extensions/manager/stores/comfyManagerStore.ts +++ b/src/workbench/extensions/manager/stores/comfyManagerStore.ts @@ -8,7 +8,7 @@ import { useCachedRequest } from '@/composables/useCachedRequest' import { useServerLogs } from '@/composables/useServerLogs' import { api } from '@/scripts/api' import { app } from '@/scripts/app' -import { useDialogService } from '@/services/dialogService' + import { normalizePackKeys } from '@/utils/packUtils' import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue' import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService' @@ -32,7 +32,6 @@ type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams'] export const useComfyManagerStore = defineStore('comfyManager', () => { const { t } = useI18n() const managerService = useComfyManagerService() - const { showManagerProgressDialog } = useDialogService() const installedPacks = ref({}) const enabledPacksIds = ref>(new Set()) @@ -204,8 +203,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => { }) try { - // Show progress dialog immediately when task is queued - showManagerProgressDialog() managerQueue.isProcessing.value = true // Prepare logging hook @@ -392,44 +389,3 @@ export const useComfyManagerStore = defineStore('comfyManager', () => { enablePack } }) - -/** - * Store for state of the manager progress dialog content. - * The dialog itself is managed by the dialog store. This store is used to - * manage the visibility of the dialog's content, header, footer. - */ -export const useManagerProgressDialogStore = defineStore( - 'managerProgressDialog', - () => { - const isExpanded = ref(false) - const activeTabIndex = ref(0) - - const setActiveTabIndex = (index: number) => { - activeTabIndex.value = index - } - - const getActiveTabIndex = () => { - return activeTabIndex.value - } - - const toggle = () => { - isExpanded.value = !isExpanded.value - } - - const collapse = () => { - isExpanded.value = false - } - - const expand = () => { - isExpanded.value = true - } - return { - isExpanded, - toggle, - collapse, - expand, - setActiveTabIndex, - getActiveTabIndex - } - } -)