mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
feat: add HoneyToast component for persistent progress notifications (#7902)
## 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 <amp@ampcode.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: sno <snomiao@gmail.com> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import type { InlineConfig } from 'vite'
|
|||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||||
addons: ['@storybook/addon-docs'],
|
addons: ['@storybook/addon-docs', '@storybook/addon-mcp'],
|
||||||
framework: {
|
framework: {
|
||||||
name: '@storybook/vue3-vite',
|
name: '@storybook/vue3-vite',
|
||||||
options: {}
|
options: {}
|
||||||
|
|||||||
@@ -122,7 +122,10 @@ The project uses **Nx** for build orchestration and task management
|
|||||||
- Prefer reactive props destructuring to `const props = defineProps<...>`
|
- Prefer reactive props destructuring to `const props = defineProps<...>`
|
||||||
- Do not use `withDefaults` or runtime props declaration
|
- Do not use `withDefaults` or runtime props declaration
|
||||||
- Do not import Vue macros unnecessarily
|
- 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
|
- 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 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`
|
- 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
|
- 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
|
- 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.
|
- 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
|
## Agent-only rules
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
"@prettier/plugin-oxc": "catalog:",
|
"@prettier/plugin-oxc": "catalog:",
|
||||||
"@sentry/vite-plugin": "catalog:",
|
"@sentry/vite-plugin": "catalog:",
|
||||||
"@storybook/addon-docs": "catalog:",
|
"@storybook/addon-docs": "catalog:",
|
||||||
|
"@storybook/addon-mcp": "catalog:",
|
||||||
"@storybook/vue3": "catalog:",
|
"@storybook/vue3": "catalog:",
|
||||||
"@storybook/vue3-vite": "catalog:",
|
"@storybook/vue3-vite": "catalog:",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
|
|||||||
134
pnpm-lock.yaml
generated
134
pnpm-lock.yaml
generated
@@ -84,6 +84,9 @@ catalogs:
|
|||||||
'@storybook/addon-docs':
|
'@storybook/addon-docs':
|
||||||
specifier: ^10.1.9
|
specifier: ^10.1.9
|
||||||
version: 10.1.9
|
version: 10.1.9
|
||||||
|
'@storybook/addon-mcp':
|
||||||
|
specifier: 0.1.6
|
||||||
|
version: 0.1.6
|
||||||
'@storybook/vue3':
|
'@storybook/vue3':
|
||||||
specifier: ^10.1.9
|
specifier: ^10.1.9
|
||||||
version: 10.1.9
|
version: 10.1.9
|
||||||
@@ -549,6 +552,9 @@ importers:
|
|||||||
'@storybook/addon-docs':
|
'@storybook/addon-docs':
|
||||||
specifier: 'catalog:'
|
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))
|
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':
|
'@storybook/vue3':
|
||||||
specifier: 'catalog:'
|
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))
|
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:
|
peerDependencies:
|
||||||
storybook: ^10.1.9
|
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':
|
'@storybook/builder-vite@10.1.9':
|
||||||
resolution: {integrity: sha512-rUILpjGV7gKfXrUeZzpNAer9PspB3LJI1d+gJHISx2Gs24bdneA3y/gu0fWw46ccOSIcwb91xoK5QxliJcWsWg==}
|
resolution: {integrity: sha512-rUILpjGV7gKfXrUeZzpNAer9PspB3LJI1d+gJHISx2Gs24bdneA3y/gu0fWw46ccOSIcwb91xoK5QxliJcWsWg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3181,6 +3192,9 @@ packages:
|
|||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
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
|
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':
|
'@storybook/react-dom-shim@10.1.9':
|
||||||
resolution: {integrity: sha512-gJsR6fI1gG4DSin6sQx8RmGDQF8Lije0cZbxHyVedNleBsveGXIPFUKFVi+pRNdwBPni1Z2g/gYyHzkOEqPD2w==}
|
resolution: {integrity: sha512-gJsR6fI1gG4DSin6sQx8RmGDQF8Lije0cZbxHyVedNleBsveGXIPFUKFVi+pRNdwBPni1Z2g/gYyHzkOEqPD2w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3453,6 +3467,26 @@ packages:
|
|||||||
'@tiptap/starter-kit@2.10.4':
|
'@tiptap/starter-kit@2.10.4':
|
||||||
resolution: {integrity: sha512-tu/WCs9Mkr5Nt8c3/uC4VvAbQlVX0OY7ygcqdzHGUeG9zP3twdW7o5xM3kyDKR2++sbVzqu5Ll5qNU+1JZvPGQ==}
|
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':
|
'@trivago/prettier-plugin-sort-imports@5.2.2':
|
||||||
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
|
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
|
||||||
engines: {node: '>18.12'}
|
engines: {node: '>18.12'}
|
||||||
@@ -3786,6 +3820,11 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
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':
|
'@vitejs/plugin-vue@6.0.3':
|
||||||
resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==}
|
resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -5170,6 +5209,9 @@ packages:
|
|||||||
jiti:
|
jiti:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
esm-env@1.2.2:
|
||||||
|
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||||
|
|
||||||
esm-resolve@1.0.11:
|
esm-resolve@1.0.11:
|
||||||
resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==}
|
resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==}
|
||||||
|
|
||||||
@@ -5978,6 +6020,9 @@ packages:
|
|||||||
json-parse-even-better-errors@2.3.1:
|
json-parse-even-better-errors@2.3.1:
|
||||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
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:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
@@ -7386,6 +7431,9 @@ packages:
|
|||||||
sprintf-js@1.0.3:
|
sprintf-js@1.0.3:
|
||||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||||
|
|
||||||
|
sqids@0.3.0:
|
||||||
|
resolution: {integrity: sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw==}
|
||||||
|
|
||||||
stable-hash-x@0.2.0:
|
stable-hash-x@0.2.0:
|
||||||
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
|
resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -7613,6 +7661,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==}
|
resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tmcp@1.19.0:
|
||||||
|
resolution: {integrity: sha512-wOY449EdaWDo7wLZEOVjeH9fn/AqfFF4f+3pDerCI8xHpy2Z8msUjAF0Vkg01aEFIdFMmiNDiY4hu6E7jVX79w==}
|
||||||
|
|
||||||
tmp@0.2.5:
|
tmp@0.2.5:
|
||||||
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
@@ -7858,6 +7909,9 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
uri-template-matcher@1.1.2:
|
||||||
|
resolution: {integrity: sha512-uZc1h12jdO3m/R77SfTEOuo6VbMhgWznaawKpBjRGSJb7i91x5PgI37NQJtG+Cerxkk0yr1pylBY2qG1kQ+aEQ==}
|
||||||
|
|
||||||
use-sync-external-store@1.6.0:
|
use-sync-external-store@1.6.0:
|
||||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7873,6 +7927,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
valibot@1.2.0:
|
||||||
|
resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vfile-message@4.0.3:
|
vfile-message@4.0.3:
|
||||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||||
|
|
||||||
@@ -8019,6 +8081,9 @@ packages:
|
|||||||
vue-component-type-helpers@3.2.1:
|
vue-component-type-helpers@3.2.1:
|
||||||
resolution: {integrity: sha512-gKV7XOkQl4urSuLHNY1tnVQf7wVgtb/mKbRyxSLWGZUY9RK7aDPhBenTjm+i8ZFe0zC2PZeHMPtOZXZfyaFOzQ==}
|
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:
|
vue-demi@0.14.10:
|
||||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -10949,6 +11014,18 @@ snapshots:
|
|||||||
- vite
|
- vite
|
||||||
- webpack
|
- 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))':
|
'@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:
|
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))
|
'@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: 19.2.3
|
||||||
react-dom: 19.2.3(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))':
|
'@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:
|
dependencies:
|
||||||
react: 19.2.3
|
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)
|
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
|
type-fest: 2.19.0
|
||||||
vue: 3.5.13(typescript@5.9.3)
|
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':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -11275,6 +11362,23 @@ snapshots:
|
|||||||
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
|
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@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)':
|
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/generator': 7.28.5
|
'@babel/generator': 7.28.5
|
||||||
@@ -11623,6 +11727,10 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||||
optional: true
|
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))':
|
'@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:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||||
@@ -13303,6 +13411,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
esm-env@1.2.2: {}
|
||||||
|
|
||||||
esm-resolve@1.0.11: {}
|
esm-resolve@1.0.11: {}
|
||||||
|
|
||||||
espree@10.4.0:
|
espree@10.4.0:
|
||||||
@@ -14189,6 +14299,8 @@ snapshots:
|
|||||||
|
|
||||||
json-parse-even-better-errors@2.3.1: {}
|
json-parse-even-better-errors@2.3.1: {}
|
||||||
|
|
||||||
|
json-rpc-2.0@1.7.1: {}
|
||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
json-schema-traverse@1.0.0: {}
|
json-schema-traverse@1.0.0: {}
|
||||||
@@ -16055,6 +16167,8 @@ snapshots:
|
|||||||
|
|
||||||
sprintf-js@1.0.3: {}
|
sprintf-js@1.0.3: {}
|
||||||
|
|
||||||
|
sqids@0.3.0: {}
|
||||||
|
|
||||||
stable-hash-x@0.2.0: {}
|
stable-hash-x@0.2.0: {}
|
||||||
|
|
||||||
stack-utils@2.0.6:
|
stack-utils@2.0.6:
|
||||||
@@ -16347,6 +16461,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tldts-core: 7.0.19
|
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: {}
|
tmp@0.2.5: {}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
@@ -16644,6 +16768,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
uri-template-matcher@1.1.2: {}
|
||||||
|
|
||||||
use-sync-external-store@1.6.0(react@19.2.3):
|
use-sync-external-store@1.6.0(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
@@ -16654,6 +16780,10 @@ snapshots:
|
|||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
|
valibot@1.2.0(typescript@5.9.3):
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 5.9.3
|
||||||
|
|
||||||
vfile-message@4.0.3:
|
vfile-message@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@@ -16914,6 +17044,8 @@ snapshots:
|
|||||||
|
|
||||||
vue-component-type-helpers@3.2.1: {}
|
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)):
|
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.13(typescript@5.9.3)
|
vue: 3.5.13(typescript@5.9.3)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ catalog:
|
|||||||
'@sentry/vue': ^10.32.1
|
'@sentry/vue': ^10.32.1
|
||||||
'@sparkjsdev/spark': ^0.1.10
|
'@sparkjsdev/spark': ^0.1.10
|
||||||
'@storybook/addon-docs': ^10.1.9
|
'@storybook/addon-docs': ^10.1.9
|
||||||
|
'@storybook/addon-mcp': 0.1.6
|
||||||
'@storybook/vue3': ^10.1.9
|
'@storybook/vue3': ^10.1.9
|
||||||
'@storybook/vue3-vite': ^10.1.9
|
'@storybook/vue3-vite': ^10.1.9
|
||||||
'@tailwindcss/vite': ^4.1.12
|
'@tailwindcss/vite': ^4.1.12
|
||||||
|
|||||||
292
src/components/honeyToast/HoneyToast.stories.ts
Normal file
292
src/components/honeyToast/HoneyToast.stories.ts
Normal file
@@ -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> = {}): AssetDownload {
|
||||||
|
return {
|
||||||
|
taskId: 'task-1',
|
||||||
|
assetId: 'asset-1',
|
||||||
|
assetName: 'model-v1.safetensors',
|
||||||
|
bytesTotal: 1000000,
|
||||||
|
bytesDownloaded: 0,
|
||||||
|
progress: 0,
|
||||||
|
status: 'created',
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta: Meta<typeof HoneyToast> = {
|
||||||
|
title: 'Toast/HoneyToast',
|
||||||
|
component: HoneyToast,
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen'
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div class="h-screen bg-base-background p-8"><story /></div>'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<HoneyToast v-model:expanded="isExpanded" :visible="true">
|
||||||
|
<template #default>
|
||||||
|
<div class="flex h-12 items-center justify-between border-b border-border-default px-4">
|
||||||
|
<h3 class="text-sm font-bold text-base-foreground">Download Queue</h3>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-h-[300px] overflow-y-auto px-4 py-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<ProgressToastItem v-for="job in jobs" :key="job.taskId" :job="job" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer="{ toggle }">
|
||||||
|
<div class="flex h-12 items-center justify-between border-t border-border-default px-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<i class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground" />
|
||||||
|
<span class="font-bold text-base-foreground">lora-style.safetensors</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-muted-foreground">1 of 3</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button variant="muted-textonly" size="icon" @click.stop="toggle">
|
||||||
|
<i :class="cn('size-4', isExpanded ? 'icon-[lucide--chevron-down]' : 'icon-[lucide--chevron-up]')" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<HoneyToast v-model:expanded="isExpanded" :visible="true">
|
||||||
|
<template #default>
|
||||||
|
<div class="flex h-12 items-center justify-between border-b border-border-default px-4">
|
||||||
|
<h3 class="text-sm font-bold text-base-foreground">Download Queue</h3>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-h-[300px] overflow-y-auto px-4 py-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<ProgressToastItem v-for="job in jobs" :key="job.taskId" :job="job" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer="{ toggle }">
|
||||||
|
<div class="flex h-12 items-center justify-between border-t border-border-default px-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<i class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground" />
|
||||||
|
<span class="font-bold text-base-foreground">lora-style.safetensors</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-muted-foreground">1 of 3</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button variant="muted-textonly" size="icon" @click.stop="toggle">
|
||||||
|
<i :class="cn('size-4', isExpanded ? 'icon-[lucide--chevron-down]' : 'icon-[lucide--chevron-up]')" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<HoneyToast v-model:expanded="isExpanded" :visible="true">
|
||||||
|
<template #default>
|
||||||
|
<div class="flex h-12 items-center justify-between border-b border-border-default px-4">
|
||||||
|
<h3 class="text-sm font-bold text-base-foreground">Download Queue</h3>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-h-[300px] overflow-y-auto px-4 py-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<ProgressToastItem v-for="job in jobs" :key="job.taskId" :job="job" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer="{ toggle }">
|
||||||
|
<div class="flex h-12 items-center justify-between border-t border-border-default px-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<i class="icon-[lucide--check-circle] size-4 text-jade-600" />
|
||||||
|
<span class="font-bold text-base-foreground">All downloads completed</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button variant="muted-textonly" size="icon" @click.stop="toggle">
|
||||||
|
<i :class="cn('size-4', isExpanded ? 'icon-[lucide--chevron-down]' : 'icon-[lucide--chevron-up]')" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="muted-textonly" size="icon">
|
||||||
|
<i class="icon-[lucide--x] size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<HoneyToast v-model:expanded="isExpanded" :visible="true">
|
||||||
|
<template #default>
|
||||||
|
<div class="flex h-12 items-center justify-between border-b border-border-default px-4">
|
||||||
|
<h3 class="text-sm font-bold text-base-foreground">Download Queue</h3>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-h-[300px] overflow-y-auto px-4 py-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<ProgressToastItem v-for="job in jobs" :key="job.taskId" :job="job" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer="{ toggle }">
|
||||||
|
<div class="flex h-12 items-center justify-between border-t border-border-default px-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<i class="icon-[lucide--circle-alert] size-4 text-destructive-background" />
|
||||||
|
<span class="font-bold text-base-foreground">1 download failed</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button variant="muted-textonly" size="icon" @click.stop="toggle">
|
||||||
|
<i :class="cn('size-4', isExpanded ? 'icon-[lucide--chevron-down]' : 'icon-[lucide--chevron-up]')" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="muted-textonly" size="icon">
|
||||||
|
<i class="icon-[lucide--x] size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Hidden: Story = {
|
||||||
|
render: () => ({
|
||||||
|
components: { HoneyToast },
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<p class="text-base-foreground">HoneyToast is hidden when visible=false. Nothing appears at the bottom.</p>
|
||||||
|
|
||||||
|
<HoneyToast :visible="false">
|
||||||
|
<template #default>
|
||||||
|
<div class="px-4 py-4">Content</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="h-12 px-4">Footer</div>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
137
src/components/honeyToast/HoneyToast.test.ts
Normal file
137
src/components/honeyToast/HoneyToast.test.ts
Normal file
@@ -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: `
|
||||||
|
<HoneyToast :visible="true" v-model:expanded="expanded">
|
||||||
|
<template #default="slotProps">
|
||||||
|
<div data-testid="content">{{ slotProps.isExpanded ? 'expanded' : 'collapsed' }}</div>
|
||||||
|
</template>
|
||||||
|
<template #footer="slotProps">
|
||||||
|
<button data-testid="toggle-btn" @click="slotProps.toggle">
|
||||||
|
{{ slotProps.isExpanded ? 'Collapse' : 'Expand' }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
46
src/components/honeyToast/HoneyToast.vue
Normal file
46
src/components/honeyToast/HoneyToast.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
const { visible } = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isExpanded = defineModel<boolean>('expanded', { default: false })
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
isExpanded.value = !isExpanded.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-300 ease-out"
|
||||||
|
enter-from-class="translate-y-full opacity-0"
|
||||||
|
enter-to-class="translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
|
leave-to-class="translate-y-full opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
class="fixed inset-x-0 bottom-6 z-50 mx-auto w-4/5 max-w-3xl overflow-hidden rounded-lg border border-border-default bg-base-background shadow-lg"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'overflow-hidden transition-all duration-300',
|
||||||
|
isExpanded ? 'max-h-[400px]' : 'max-h-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot :is-expanded />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="footer" :is-expanded :toggle />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
94
src/components/toast/ProgressToastItem.stories.ts
Normal file
94
src/components/toast/ProgressToastItem.stories.ts
Normal file
@@ -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<typeof ProgressToastItem> = {
|
||||||
|
title: 'Toast/ProgressToastItem',
|
||||||
|
component: ProgressToastItem,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded'
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
() => ({
|
||||||
|
template: '<div class="w-[400px] bg-base-background p-4"><story /></div>'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
function createMockJob(overrides: Partial<AssetDownload> = {}): 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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,9 +30,7 @@ const isPending = computed(() => job.status === 'created')
|
|||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm text-base-foreground">{{ job.assetName }}</span>
|
<span class="text-sm text-base-foreground">{{ job.assetName }}</span>
|
||||||
<span v-if="isRunning" class="text-xs text-muted-foreground">
|
<span v-if="isRunning" class="text-xs text-muted-foreground"> </span>
|
||||||
{{ progressPercent }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -49,9 +47,9 @@ const isPending = computed(() => job.status === 'created')
|
|||||||
|
|
||||||
<template v-else-if="isRunning">
|
<template v-else-if="isRunning">
|
||||||
<i
|
<i
|
||||||
class="icon-[lucide--loader-circle] size-4 animate-spin text-primary-background"
|
class="icon-[lucide--loader-circle] size-4 animate-spin text-base-foreground"
|
||||||
/>
|
/>
|
||||||
<span class="text-xs text-primary-background">
|
<span class="text-xs text-base-foreground">
|
||||||
{{ progressPercent }}%
|
{{ progressPercent }}%
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -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',
|
id: 'Comfy.User.OpenSignInDialog',
|
||||||
icon: 'pi pi-user',
|
icon: 'pi pi-user',
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { whenever } from '@vueuse/core'
|
||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
||||||
import ProgressToastItem from '@/components/toast/ProgressToastItem.vue'
|
import ProgressToastItem from '@/components/toast/ProgressToastItem.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
|
import { useAssetDownloadStore } from '@/stores/assetDownloadStore'
|
||||||
@@ -17,12 +19,10 @@ const isExpanded = ref(false)
|
|||||||
const activeFilter = ref<'all' | 'completed' | 'failed'>('all')
|
const activeFilter = ref<'all' | 'completed' | 'failed'>('all')
|
||||||
const filterPopoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
const filterPopoverRef = ref<InstanceType<typeof Popover> | null>(null)
|
||||||
|
|
||||||
function toggle() {
|
whenever(
|
||||||
isExpanded.value = !isExpanded.value
|
() => !isExpanded.value,
|
||||||
if (!isExpanded.value) {
|
() => filterPopoverRef.value?.hide()
|
||||||
filterPopoverRef.value?.hide()
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterOptions = [
|
const filterOptions = [
|
||||||
{ value: 'all', label: 'all' },
|
{ value: 'all', label: 'all' },
|
||||||
@@ -83,189 +83,168 @@ function closeDialog() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<HoneyToast v-model:expanded="isExpanded" :visible>
|
||||||
<Transition
|
<template #default>
|
||||||
enter-active-class="transition-all duration-300 ease-out"
|
|
||||||
enter-from-class="translate-y-full opacity-0"
|
|
||||||
enter-to-class="translate-y-0 opacity-100"
|
|
||||||
leave-active-class="transition-all duration-200 ease-in"
|
|
||||||
leave-from-class="translate-y-0 opacity-100"
|
|
||||||
leave-to-class="translate-y-full opacity-0"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-if="visible"
|
class="flex h-12 items-center justify-between border-b border-border-default px-4"
|
||||||
class="fixed inset-x-0 bottom-6 z-50 mx-auto w-[80%] max-w-3xl overflow-hidden rounded-lg border border-border-default bg-base-background shadow-lg"
|
|
||||||
>
|
>
|
||||||
<div
|
<h3 class="text-sm font-bold text-base-foreground">
|
||||||
:class="
|
{{ t('progressToast.importingModels') }}
|
||||||
cn(
|
</h3>
|
||||||
'overflow-hidden transition-all duration-300',
|
<div class="flex items-center gap-2">
|
||||||
isExpanded ? 'max-h-[400px]' : 'max-h-0'
|
<Button
|
||||||
)
|
variant="secondary"
|
||||||
"
|
size="md"
|
||||||
>
|
class="gap-1.5 px-2"
|
||||||
<div
|
@click="onFilterClick"
|
||||||
class="flex h-12 items-center justify-between border-b border-border-default px-4"
|
>
|
||||||
|
<i class="icon-[lucide--list-filter] size-4" />
|
||||||
|
<span>{{ activeFilterLabel }}</span>
|
||||||
|
<i class="icon-[lucide--chevron-down] size-3" />
|
||||||
|
</Button>
|
||||||
|
<Popover
|
||||||
|
ref="filterPopoverRef"
|
||||||
|
append-to="body"
|
||||||
|
:dismissable="true"
|
||||||
|
:close-on-escape="true"
|
||||||
|
unstyled
|
||||||
|
:pt="{
|
||||||
|
root: { class: 'absolute z-50' },
|
||||||
|
content: {
|
||||||
|
class:
|
||||||
|
'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg'
|
||||||
|
}
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<h3 class="text-sm font-bold text-base-foreground">
|
|
||||||
{{ t('progressToast.importingModels') }}
|
|
||||||
</h3>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
class="gap-1.5 px-2"
|
|
||||||
@click="onFilterClick"
|
|
||||||
>
|
|
||||||
<i class="icon-[lucide--list-filter] size-4" />
|
|
||||||
<span>{{ activeFilterLabel }}</span>
|
|
||||||
<i class="icon-[lucide--chevron-down] size-3" />
|
|
||||||
</Button>
|
|
||||||
<Popover
|
|
||||||
ref="filterPopoverRef"
|
|
||||||
append-to="body"
|
|
||||||
:dismissable="true"
|
|
||||||
:close-on-escape="true"
|
|
||||||
unstyled
|
|
||||||
:pt="{
|
|
||||||
root: { class: 'absolute z-50' },
|
|
||||||
content: {
|
|
||||||
class:
|
|
||||||
'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg'
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex min-w-[120px] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
v-for="option in filterOptions"
|
|
||||||
:key="option.value"
|
|
||||||
variant="textonly"
|
|
||||||
size="sm"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'w-full justify-start bg-transparent',
|
|
||||||
activeFilter === option.value &&
|
|
||||||
'bg-secondary-background-selected'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@click="setFilter(option.value)"
|
|
||||||
>
|
|
||||||
{{ t(`progressToast.filter.${option.label}`) }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative max-h-[300px] overflow-y-auto px-4 py-4">
|
|
||||||
<div
|
<div
|
||||||
v-if="filteredJobs.length > 3"
|
class="flex min-w-30 flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
||||||
class="absolute right-1 top-4 h-12 w-1 rounded-full bg-muted-foreground"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<ProgressToastItem
|
|
||||||
v-for="job in filteredJobs"
|
|
||||||
:key="job.taskId"
|
|
||||||
:job="job"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="filteredJobs.length === 0"
|
|
||||||
class="flex flex-col items-center justify-center py-6 text-center"
|
|
||||||
>
|
>
|
||||||
<span class="text-sm text-muted-foreground">
|
<Button
|
||||||
{{
|
v-for="option in filterOptions"
|
||||||
t('progressToast.noImportsInQueue', {
|
:key="option.value"
|
||||||
filter: activeFilterLabel
|
variant="textonly"
|
||||||
})
|
size="sm"
|
||||||
}}
|
:class="
|
||||||
</span>
|
cn(
|
||||||
|
'w-full justify-start bg-transparent',
|
||||||
|
activeFilter === option.value &&
|
||||||
|
'bg-secondary-background-selected'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="setFilter(option.value)"
|
||||||
|
>
|
||||||
|
{{ t(`progressToast.filter.${option.label}`) }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative max-h-75 overflow-y-auto px-4 py-4">
|
||||||
|
<div
|
||||||
|
v-if="filteredJobs.length > 3"
|
||||||
|
class="absolute right-1 top-4 h-12 w-1 rounded-full bg-muted-foreground"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<ProgressToastItem
|
||||||
|
v-for="job in filteredJobs"
|
||||||
|
:key="job.taskId"
|
||||||
|
:job="job"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex h-12 items-center justify-between border-t border-border-default px-4"
|
v-if="filteredJobs.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center py-6 text-center"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<span class="text-sm text-muted-foreground">
|
||||||
<template v-if="isInProgress">
|
{{
|
||||||
<i
|
t('progressToast.noImportsInQueue', {
|
||||||
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
|
filter: activeFilterLabel
|
||||||
/>
|
})
|
||||||
<span class="font-bold text-base-foreground">{{
|
}}
|
||||||
currentJobName
|
</span>
|
||||||
}}</span>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<template v-else-if="failedJobs.length > 0">
|
</template>
|
||||||
<i
|
|
||||||
class="icon-[lucide--circle-alert] size-4 text-destructive-background"
|
|
||||||
/>
|
|
||||||
<span class="font-bold text-base-foreground">
|
|
||||||
{{
|
|
||||||
t('progressToast.downloadsFailed', {
|
|
||||||
count: failedJobs.length
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<i class="icon-[lucide--check-circle] size-4 text-jade-600" />
|
|
||||||
<span class="font-bold text-base-foreground">
|
|
||||||
{{ t('progressToast.allDownloadsCompleted') }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<template #footer="{ toggle }">
|
||||||
<span v-if="isInProgress" class="text-sm text-muted-foreground">
|
<div
|
||||||
|
class="flex h-12 items-center justify-between border-t border-border-default px-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<template v-if="isInProgress">
|
||||||
|
<i
|
||||||
|
class="icon-[lucide--loader-circle] size-4 animate-spin text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<span class="font-bold text-base-foreground">{{
|
||||||
|
currentJobName
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="failedJobs.length > 0">
|
||||||
|
<i
|
||||||
|
class="icon-[lucide--circle-alert] size-4 text-destructive-background"
|
||||||
|
/>
|
||||||
|
<span class="font-bold text-base-foreground">
|
||||||
{{
|
{{
|
||||||
t('progressToast.progressCount', {
|
t('progressToast.downloadsFailed', {
|
||||||
completed: completedCount,
|
count: failedJobs.length
|
||||||
total: totalCount
|
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<i class="icon-[lucide--check-circle] size-4 text-jade-600" />
|
||||||
|
<span class="font-bold text-base-foreground">
|
||||||
|
{{ t('progressToast.allDownloadsCompleted') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<span v-if="isInProgress" class="text-sm text-muted-foreground">
|
||||||
variant="muted-textonly"
|
{{
|
||||||
size="icon"
|
t('progressToast.progressCount', {
|
||||||
:aria-label="
|
completed: completedCount,
|
||||||
isExpanded
|
total: totalCount
|
||||||
? t('contextMenu.Collapse')
|
})
|
||||||
: t('contextMenu.Expand')
|
}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="muted-textonly"
|
||||||
|
size="icon"
|
||||||
|
:aria-label="
|
||||||
|
isExpanded ? t('contextMenu.Collapse') : t('contextMenu.Expand')
|
||||||
|
"
|
||||||
|
@click.stop="toggle"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'size-4',
|
||||||
|
isExpanded
|
||||||
|
? 'icon-[lucide--chevron-down]'
|
||||||
|
: 'icon-[lucide--chevron-up]'
|
||||||
|
)
|
||||||
"
|
"
|
||||||
@click.stop="toggle"
|
/>
|
||||||
>
|
</Button>
|
||||||
<i
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'size-4',
|
|
||||||
isExpanded
|
|
||||||
? 'icon-[lucide--chevron-down]'
|
|
||||||
: 'icon-[lucide--chevron-up]'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
v-if="!isInProgress"
|
v-if="!isInProgress"
|
||||||
variant="muted-textonly"
|
variant="muted-textonly"
|
||||||
size="icon"
|
size="icon"
|
||||||
:aria-label="t('g.close')"
|
:aria-label="t('g.close')"
|
||||||
@click.stop="closeDialog"
|
@click.stop="closeDialog"
|
||||||
>
|
>
|
||||||
<i class="icon-[lucide--x] size-4" />
|
<i class="icon-[lucide--x] size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</template>
|
||||||
</Teleport>
|
</HoneyToast>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ import type {
|
|||||||
DialogComponentProps,
|
DialogComponentProps,
|
||||||
ShowDialogOptions
|
ShowDialogOptions
|
||||||
} from '@/stores/dialogStore'
|
} 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 ManagerDialogContent from '@/workbench/extensions/manager/components/manager/ManagerDialogContent.vue'
|
||||||
import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue'
|
import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue'
|
||||||
import ImportFailedNodeContent from '@/workbench/extensions/manager/components/manager/ImportFailedNodeContent.vue'
|
import ImportFailedNodeContent from '@/workbench/extensions/manager/components/manager/ImportFailedNodeContent.vue'
|
||||||
@@ -232,30 +230,6 @@ export const useDialogService = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function showManagerProgressDialog(options?: {
|
|
||||||
props?: InstanceType<typeof ManagerProgressDialogContent>['$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
|
* Shows a dialog requiring sign in for API nodes
|
||||||
* @returns Promise that resolves to true if user clicks login, false if cancelled
|
* @returns Promise that resolves to true if user clicks login, false if cancelled
|
||||||
@@ -443,16 +417,6 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleManagerProgressDialog(
|
|
||||||
props?: ComponentAttrs<typeof ManagerProgressDialogContent>
|
|
||||||
) {
|
|
||||||
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
|
|
||||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
|
||||||
} else {
|
|
||||||
showManagerProgressDialog({ props })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLayoutDialog(options: {
|
function showLayoutDialog(options: {
|
||||||
key: string
|
key: string
|
||||||
component: Component
|
component: Component
|
||||||
@@ -588,7 +552,6 @@ export const useDialogService = () => {
|
|||||||
showAboutDialog,
|
showAboutDialog,
|
||||||
showExecutionErrorDialog,
|
showExecutionErrorDialog,
|
||||||
showManagerDialog,
|
showManagerDialog,
|
||||||
showManagerProgressDialog,
|
|
||||||
showApiNodesSignInDialog,
|
showApiNodesSignInDialog,
|
||||||
showSignInDialog,
|
showSignInDialog,
|
||||||
showSubscriptionRequiredDialog,
|
showSubscriptionRequiredDialog,
|
||||||
@@ -599,7 +562,6 @@ export const useDialogService = () => {
|
|||||||
showErrorDialog,
|
showErrorDialog,
|
||||||
confirm,
|
confirm,
|
||||||
toggleManagerDialog,
|
toggleManagerDialog,
|
||||||
toggleManagerProgressDialog,
|
|
||||||
showLayoutDialog,
|
showLayoutDialog,
|
||||||
showImportFailedNodeDialog,
|
showImportFailedNodeDialog,
|
||||||
showNodeConflictDialog
|
showNodeConflictDialog
|
||||||
|
|||||||
@@ -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 |
|
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
|
||||||
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
|
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
|
||||||
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
|
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
|
||||||
| comfyManagerStore.ts | useManagerProgressDialogStore | Manages manager progress dialog state | UI |
|
|
||||||
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
|
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
|
||||||
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
|
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
|
||||||
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
|
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<GlobalToast />
|
<GlobalToast />
|
||||||
<RerouteMigrationToast />
|
<RerouteMigrationToast />
|
||||||
<ModelImportProgressDialog />
|
<ModelImportProgressDialog />
|
||||||
|
<ManagerProgressToast />
|
||||||
<UnloadWindowConfirmDialog v-if="!isElectron()" />
|
<UnloadWindowConfirmDialog v-if="!isElectron()" />
|
||||||
<MenuHamburger />
|
<MenuHamburger />
|
||||||
</template>
|
</template>
|
||||||
@@ -80,6 +81,7 @@ import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
|||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||||
import LinearView from '@/views/LinearView.vue'
|
import LinearView from '@/views/LinearView.vue'
|
||||||
|
import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue'
|
||||||
|
|
||||||
setupAutoQueueHandler()
|
setupAutoQueueHandler()
|
||||||
useProgressFavicon()
|
useProgressFavicon()
|
||||||
|
|||||||
@@ -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<typeof ManagerProgressDialogContent> & {
|
|
||||||
lastPanelRef: HTMLElement | null
|
|
||||||
onLogsAdded: () => void
|
|
||||||
handleScroll: (e: { target: HTMLElement }) => void
|
|
||||||
isUserScrolling: boolean
|
|
||||||
resetUserScrolling: () => void
|
|
||||||
collapsedPanels: Record<number, boolean>
|
|
||||||
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<string, any> = {}): VueWrapper<ComponentInstance> => {
|
|
||||||
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<ComponentInstance>
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="overflow-hidden transition-all duration-300"
|
|
||||||
:class="{
|
|
||||||
'max-h-[500px]': isExpanded,
|
|
||||||
'm-0 max-h-0 p-0': !isExpanded
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref="sectionsContainerRef"
|
|
||||||
class="scroll-container max-h-[450px] overflow-y-auto px-6 py-4"
|
|
||||||
:style="{
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
scrollbarColor: 'rgba(156, 163, 175, 0.5) transparent'
|
|
||||||
}"
|
|
||||||
:class="{
|
|
||||||
'max-h-[450px]': isExpanded,
|
|
||||||
'max-h-0': !isExpanded
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div v-for="(log, index) in focusedLogs" :key="index">
|
|
||||||
<Panel
|
|
||||||
:expanded="collapsedPanels[index] === true"
|
|
||||||
toggleable
|
|
||||||
class="shadow-elevation-1 mt-2 rounded-lg"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="flex w-full items-center justify-between py-2">
|
|
||||||
<div class="flex flex-col text-sm leading-normal font-medium">
|
|
||||||
<span>{{ log.taskName }}</span>
|
|
||||||
<span class="text-muted">
|
|
||||||
{{
|
|
||||||
isInProgress(index)
|
|
||||||
? $t('g.inProgress')
|
|
||||||
: $t('g.completed') + ' ✓'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #toggleicon>
|
|
||||||
<Button
|
|
||||||
variant="textonly"
|
|
||||||
class="text-neutral-300"
|
|
||||||
@click="togglePanel(index)"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
:class="
|
|
||||||
collapsedPanels[index]
|
|
||||||
? 'pi pi-chevron-right'
|
|
||||||
: 'pi pi-chevron-down'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
:ref="
|
|
||||||
index === focusedLogs.length - 1
|
|
||||||
? (el) => (lastPanelRef = el as HTMLElement)
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
class="h-64 overflow-y-auto rounded-lg bg-black"
|
|
||||||
:class="{
|
|
||||||
'h-64': index !== focusedLogs.length - 1,
|
|
||||||
grow: index === focusedLogs.length - 1
|
|
||||||
}"
|
|
||||||
@scroll="handleScroll"
|
|
||||||
>
|
|
||||||
<div class="h-full">
|
|
||||||
<div
|
|
||||||
v-for="(logLine, logIndex) in log.logs"
|
|
||||||
:key="logIndex"
|
|
||||||
class="text-muted"
|
|
||||||
>
|
|
||||||
<pre class="break-words whitespace-pre-wrap">{{ logLine }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Panel>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useScroll, whenever } from '@vueuse/core'
|
|
||||||
import Panel from 'primevue/panel'
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
|
||||||
import {
|
|
||||||
useComfyManagerStore,
|
|
||||||
useManagerProgressDialogStore
|
|
||||||
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
|
||||||
|
|
||||||
const comfyManagerStore = useComfyManagerStore()
|
|
||||||
const progressDialogContent = useManagerProgressDialogStore()
|
|
||||||
|
|
||||||
const isInProgress = (index: number) => {
|
|
||||||
const log = focusedLogs.value[index]
|
|
||||||
if (!log) return false
|
|
||||||
|
|
||||||
// Check if this task is in the running or pending queue
|
|
||||||
const taskQueue = comfyManagerStore.taskQueue
|
|
||||||
if (!taskQueue) return false
|
|
||||||
|
|
||||||
const allQueueTasks = [
|
|
||||||
...(taskQueue.running_queue || []),
|
|
||||||
...(taskQueue.pending_queue || [])
|
|
||||||
]
|
|
||||||
|
|
||||||
return allQueueTasks.some((task) => task.ui_id === log.taskId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusedLogs = computed(() => {
|
|
||||||
if (progressDialogContent.getActiveTabIndex() === 0) {
|
|
||||||
return comfyManagerStore.succeededTasksLogs
|
|
||||||
}
|
|
||||||
return comfyManagerStore.failedTasksLogs
|
|
||||||
})
|
|
||||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
|
||||||
const isCollapsed = computed(() => !isExpanded.value)
|
|
||||||
|
|
||||||
const collapsedPanels = ref<Record<number, boolean>>({})
|
|
||||||
const togglePanel = (index: number) => {
|
|
||||||
collapsedPanels.value[index] = !collapsedPanels.value[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionsContainerRef = ref<HTMLElement | null>(null)
|
|
||||||
const { y: scrollY } = useScroll(sectionsContainerRef, {
|
|
||||||
eventListenerOptions: {
|
|
||||||
passive: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const lastPanelRef = ref<HTMLElement | null>(null)
|
|
||||||
const isUserScrolling = ref(false)
|
|
||||||
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
|
|
||||||
|
|
||||||
const isAtBottom = (el: HTMLElement | null) => {
|
|
||||||
if (!el) return false
|
|
||||||
const threshold = 20
|
|
||||||
return Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollLastPanelToBottom = () => {
|
|
||||||
if (!lastPanelRef.value || isUserScrolling.value) return
|
|
||||||
lastPanelRef.value.scrollTop = lastPanelRef.value.scrollHeight
|
|
||||||
}
|
|
||||||
const scrollContentToBottom = () => {
|
|
||||||
scrollY.value = sectionsContainerRef.value?.scrollHeight ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetUserScrolling = () => {
|
|
||||||
isUserScrolling.value = false
|
|
||||||
}
|
|
||||||
const handleScroll = (e: Event) => {
|
|
||||||
const target = e.target as HTMLElement
|
|
||||||
if (target !== lastPanelRef.value) return
|
|
||||||
|
|
||||||
isUserScrolling.value = !isAtBottom(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onLogsAdded = () => {
|
|
||||||
// If user is scrolling manually, don't automatically scroll to bottom
|
|
||||||
if (isUserScrolling.value) return
|
|
||||||
|
|
||||||
scrollLastPanelToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
whenever(lastPanelLogs, onLogsAdded, { flush: 'post', deep: true })
|
|
||||||
whenever(() => isExpanded.value, scrollContentToBottom)
|
|
||||||
whenever(isCollapsed, resetUserScrolling)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
scrollContentToBottom()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
progressDialogContent.collapse()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -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<string, any>,
|
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex w-full items-center justify-between px-6 py-2 shadow-lg"
|
|
||||||
:class="{
|
|
||||||
'rounded-t-none': progressDialogContent.isExpanded,
|
|
||||||
'rounded-lg': !progressDialogContent.isExpanded
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="flex items-center text-base leading-none">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<template v-if="isInProgress">
|
|
||||||
<DotSpinner duration="1s" class="mr-2" />
|
|
||||||
<span>{{ currentTaskName }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="isRestartCompleted">
|
|
||||||
<span class="mr-2">🎉</span>
|
|
||||||
<span>{{ currentTaskName }}</span>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<span class="mr-2">✅</span>
|
|
||||||
<span>{{ $t('manager.restartToApplyChanges') }}</span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<span v-if="isInProgress" class="text-sm text-neutral-700">
|
|
||||||
{{ completedTasksCount }} {{ $t('g.progressCountOf') }}
|
|
||||||
{{ totalTasksCount }}
|
|
||||||
</span>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Button
|
|
||||||
v-if="!isInProgress && !isRestartCompleted"
|
|
||||||
variant="secondary"
|
|
||||||
class="mr-4 rounded-full border-2 border-base-foreground px-3 text-base-foreground hover:bg-secondary-background-hover"
|
|
||||||
@click="handleRestart"
|
|
||||||
>
|
|
||||||
{{ $t('manager.applyChanges') }}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-else-if="!isRestartCompleted"
|
|
||||||
variant="muted-textonly"
|
|
||||||
size="sm"
|
|
||||||
class="rounded-full font-bold"
|
|
||||||
:aria-label="
|
|
||||||
$t(
|
|
||||||
progressDialogContent.isExpanded
|
|
||||||
? 'contextMenu.Collapse'
|
|
||||||
: 'contextMenu.Expand'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@click.stop="progressDialogContent.toggle"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
:class="
|
|
||||||
progressDialogContent.isExpanded
|
|
||||||
? 'pi pi-chevron-up'
|
|
||||||
: 'pi pi-chevron-down'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="muted-textonly"
|
|
||||||
size="sm"
|
|
||||||
class="rounded-full font-bold"
|
|
||||||
:aria-label="$t('g.close')"
|
|
||||||
@click.stop="closeDialog"
|
|
||||||
>
|
|
||||||
<i class="pi pi-times" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useEventListener } from '@vueuse/core'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
||||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
|
||||||
import { api } from '@/scripts/api'
|
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
|
||||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
|
||||||
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
|
||||||
import {
|
|
||||||
useComfyManagerStore,
|
|
||||||
useManagerProgressDialogStore
|
|
||||||
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const dialogStore = useDialogStore()
|
|
||||||
const progressDialogContent = useManagerProgressDialogStore()
|
|
||||||
const comfyManagerStore = useComfyManagerStore()
|
|
||||||
const settingStore = useSettingStore()
|
|
||||||
const { runFullConflictAnalysis } = useConflictDetection()
|
|
||||||
|
|
||||||
// State management for restart process
|
|
||||||
const isRestarting = ref<boolean>(false)
|
|
||||||
const isRestartCompleted = ref<boolean>(false)
|
|
||||||
|
|
||||||
const isInProgress = computed(
|
|
||||||
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
|
||||||
)
|
|
||||||
|
|
||||||
const completedTasksCount = computed(() => {
|
|
||||||
return (
|
|
||||||
comfyManagerStore.succeededTasksIds.length +
|
|
||||||
comfyManagerStore.failedTasksIds.length
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalTasksCount = computed(() => {
|
|
||||||
const completedTasks = Object.keys(comfyManagerStore.taskHistory).length
|
|
||||||
const taskQueue = comfyManagerStore.taskQueue
|
|
||||||
const queuedTasks = taskQueue
|
|
||||||
? (taskQueue.running_queue?.length || 0) +
|
|
||||||
(taskQueue.pending_queue?.length || 0)
|
|
||||||
: 0
|
|
||||||
return completedTasks + queuedTasks
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const fallbackTaskName = t('manager.installingDependencies')
|
|
||||||
const currentTaskName = computed(() => {
|
|
||||||
if (isRestarting.value) {
|
|
||||||
return t('manager.restartingBackend')
|
|
||||||
}
|
|
||||||
if (isRestartCompleted.value) {
|
|
||||||
return t('manager.extensionsSuccessfullyInstalled')
|
|
||||||
}
|
|
||||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
|
||||||
const task = comfyManagerStore.taskLogs.at(-1)
|
|
||||||
return task?.taskName ?? fallbackTaskName
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleRestart = async () => {
|
|
||||||
// Store original toast setting value
|
|
||||||
const originalToastSetting = settingStore.get(
|
|
||||||
'Comfy.Toast.DisableReconnectingToast'
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
|
||||||
|
|
||||||
isRestarting.value = true
|
|
||||||
|
|
||||||
const onReconnect = async () => {
|
|
||||||
try {
|
|
||||||
comfyManagerStore.setStale()
|
|
||||||
|
|
||||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
|
||||||
|
|
||||||
await useWorkflowService().reloadCurrentWorkflow()
|
|
||||||
|
|
||||||
// Run conflict detection in background after restart completion
|
|
||||||
void runFullConflictAnalysis()
|
|
||||||
} finally {
|
|
||||||
await settingStore.set(
|
|
||||||
'Comfy.Toast.DisableReconnectingToast',
|
|
||||||
originalToastSetting
|
|
||||||
)
|
|
||||||
|
|
||||||
isRestarting.value = false
|
|
||||||
isRestartCompleted.value = true
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
closeDialog()
|
|
||||||
comfyManagerStore.resetTaskState()
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
|
||||||
|
|
||||||
await useComfyManagerService().rebootComfyUI()
|
|
||||||
} catch (error) {
|
|
||||||
// If restart fails, restore settings and reset state
|
|
||||||
await settingStore.set(
|
|
||||||
'Comfy.Toast.DisableReconnectingToast',
|
|
||||||
originalToastSetting
|
|
||||||
)
|
|
||||||
isRestarting.value = false
|
|
||||||
isRestartCompleted.value = false
|
|
||||||
closeDialog() // Close dialog on error
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="progressDialogContent.isExpanded"
|
|
||||||
class="flex items-center px-4 py-2"
|
|
||||||
>
|
|
||||||
<TabMenu
|
|
||||||
v-model:active-index="activeTabIndex"
|
|
||||||
:model="tabs"
|
|
||||||
class="w-full border-none"
|
|
||||||
:pt="{
|
|
||||||
menu: { class: 'border-none' },
|
|
||||||
menuitem: { class: 'font-medium' },
|
|
||||||
action: { class: 'px-4 py-2' }
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import TabMenu from 'primevue/tabmenu'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import {
|
|
||||||
useComfyManagerStore,
|
|
||||||
useManagerProgressDialogStore
|
|
||||||
} from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
|
||||||
|
|
||||||
const progressDialogContent = useManagerProgressDialogStore()
|
|
||||||
const comfyManagerStore = useComfyManagerStore()
|
|
||||||
const activeTabIndex = computed({
|
|
||||||
get: () => progressDialogContent.getActiveTabIndex(),
|
|
||||||
set: (value) => progressDialogContent.setActiveTabIndex(value)
|
|
||||||
})
|
|
||||||
const { t } = useI18n()
|
|
||||||
const tabs = computed(() => [
|
|
||||||
{ label: t('manager.installationQueue') },
|
|
||||||
{
|
|
||||||
label: t('manager.failed', {
|
|
||||||
count: comfyManagerStore.failedTasksIds.length
|
|
||||||
})
|
|
||||||
}
|
|
||||||
])
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useEventListener, useScroll, whenever } from '@vueuse/core'
|
||||||
|
import Panel from 'primevue/panel'
|
||||||
|
import TabMenu from 'primevue/tabmenu'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||||
|
import HoneyToast from '@/components/honeyToast/HoneyToast.vue'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||||
|
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
||||||
|
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const comfyManagerStore = useComfyManagerStore()
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const { runFullConflictAnalysis } = useConflictDetection()
|
||||||
|
|
||||||
|
const isExpanded = ref(false)
|
||||||
|
const activeTabIndex = ref(0)
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ label: t('manager.installationQueue') },
|
||||||
|
{
|
||||||
|
label: t('manager.failed', {
|
||||||
|
count: comfyManagerStore.failedTasksIds.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const focusedLogs = computed(() => {
|
||||||
|
if (activeTabIndex.value === 0) {
|
||||||
|
return comfyManagerStore.succeededTasksLogs
|
||||||
|
}
|
||||||
|
return comfyManagerStore.failedTasksLogs
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = computed(() => comfyManagerStore.taskLogs.length > 0)
|
||||||
|
|
||||||
|
const isRestarting = ref(false)
|
||||||
|
const isRestartCompleted = ref(false)
|
||||||
|
|
||||||
|
const isInProgress = computed(
|
||||||
|
() => comfyManagerStore.isProcessingTasks || isRestarting.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const isTaskInProgress = (index: number) => {
|
||||||
|
const log = focusedLogs.value[index]
|
||||||
|
if (!log) return false
|
||||||
|
|
||||||
|
const taskQueue = comfyManagerStore.taskQueue
|
||||||
|
if (!taskQueue) return false
|
||||||
|
|
||||||
|
const allQueueTasks = [
|
||||||
|
...(taskQueue.running_queue || []),
|
||||||
|
...(taskQueue.pending_queue || [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return allQueueTasks.some((task) => task.ui_id === log.taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedTasksCount = computed(() => {
|
||||||
|
return (
|
||||||
|
comfyManagerStore.succeededTasksIds.length +
|
||||||
|
comfyManagerStore.failedTasksIds.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalTasksCount = computed(() => {
|
||||||
|
const completedTasks = Object.keys(comfyManagerStore.taskHistory).length
|
||||||
|
const taskQueue = comfyManagerStore.taskQueue
|
||||||
|
const queuedTasks = taskQueue
|
||||||
|
? (taskQueue.running_queue?.length || 0) +
|
||||||
|
(taskQueue.pending_queue?.length || 0)
|
||||||
|
: 0
|
||||||
|
return completedTasks + queuedTasks
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentTaskName = computed(() => {
|
||||||
|
if (isRestarting.value) {
|
||||||
|
return t('manager.restartingBackend')
|
||||||
|
}
|
||||||
|
if (isRestartCompleted.value) {
|
||||||
|
return t('manager.extensionsSuccessfullyInstalled')
|
||||||
|
}
|
||||||
|
if (!comfyManagerStore.taskLogs.length)
|
||||||
|
return t('manager.installingDependencies')
|
||||||
|
const task = comfyManagerStore.taskLogs.at(-1)
|
||||||
|
return task?.taskName ?? t('manager.installingDependencies')
|
||||||
|
})
|
||||||
|
|
||||||
|
const collapsedPanels = ref<Record<number, boolean>>({})
|
||||||
|
function togglePanel(index: number) {
|
||||||
|
collapsedPanels.value[index] = !collapsedPanels.value[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionsContainerRef = ref<HTMLElement | null>(null)
|
||||||
|
const { y: scrollY } = useScroll(sectionsContainerRef, {
|
||||||
|
eventListenerOptions: { passive: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastPanelRef = ref<HTMLElement | null>(null)
|
||||||
|
const isUserScrolling = ref(false)
|
||||||
|
const lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)
|
||||||
|
|
||||||
|
function isAtBottom(el: HTMLElement | null) {
|
||||||
|
if (!el) return false
|
||||||
|
const threshold = 20
|
||||||
|
return Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollLastPanelToBottom() {
|
||||||
|
if (!lastPanelRef.value || isUserScrolling.value) return
|
||||||
|
lastPanelRef.value.scrollTop = lastPanelRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollContentToBottom() {
|
||||||
|
scrollY.value = sectionsContainerRef.value?.scrollHeight ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUserScrolling() {
|
||||||
|
isUserScrolling.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScroll(e: Event) {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (target !== lastPanelRef.value) return
|
||||||
|
isUserScrolling.value = !isAtBottom(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogsAdded() {
|
||||||
|
if (isUserScrolling.value) return
|
||||||
|
scrollLastPanelToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
whenever(lastPanelLogs, onLogsAdded, { flush: 'post', deep: true })
|
||||||
|
whenever(() => isExpanded.value, scrollContentToBottom)
|
||||||
|
whenever(() => !isExpanded.value, resetUserScrolling)
|
||||||
|
|
||||||
|
function closeToast() {
|
||||||
|
comfyManagerStore.resetTaskState()
|
||||||
|
isExpanded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRestart() {
|
||||||
|
const originalToastSetting = settingStore.get(
|
||||||
|
'Comfy.Toast.DisableReconnectingToast'
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)
|
||||||
|
|
||||||
|
isRestarting.value = true
|
||||||
|
|
||||||
|
const onReconnect = async () => {
|
||||||
|
try {
|
||||||
|
comfyManagerStore.setStale()
|
||||||
|
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||||
|
await useWorkflowService().reloadCurrentWorkflow()
|
||||||
|
void runFullConflictAnalysis()
|
||||||
|
} finally {
|
||||||
|
await settingStore.set(
|
||||||
|
'Comfy.Toast.DisableReconnectingToast',
|
||||||
|
originalToastSetting
|
||||||
|
)
|
||||||
|
isRestarting.value = false
|
||||||
|
isRestartCompleted.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
closeToast()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||||
|
|
||||||
|
await useComfyManagerService().rebootComfyUI()
|
||||||
|
} catch (error) {
|
||||||
|
await settingStore.set(
|
||||||
|
'Comfy.Toast.DisableReconnectingToast',
|
||||||
|
originalToastSetting
|
||||||
|
)
|
||||||
|
isRestarting.value = false
|
||||||
|
isRestartCompleted.value = false
|
||||||
|
closeToast()
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollContentToBottom()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
isExpanded.value = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HoneyToast v-model:expanded="isExpanded" :visible>
|
||||||
|
<template #default>
|
||||||
|
<div v-if="isExpanded" class="flex items-center px-4 py-2">
|
||||||
|
<TabMenu
|
||||||
|
v-model:active-index="activeTabIndex"
|
||||||
|
:model="tabs"
|
||||||
|
class="w-full border-none"
|
||||||
|
:pt="{
|
||||||
|
menu: { class: 'border-none' },
|
||||||
|
menuitem: { class: 'font-medium' },
|
||||||
|
action: { class: 'px-4 py-2' }
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="sectionsContainerRef"
|
||||||
|
class="scroll-container max-h-[450px] overflow-y-auto px-6 py-4"
|
||||||
|
:style="{
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
scrollbarColor: 'rgba(156, 163, 175, 0.5) transparent'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div v-for="(log, index) in focusedLogs" :key="index">
|
||||||
|
<Panel
|
||||||
|
:expanded="collapsedPanels[index] === true"
|
||||||
|
toggleable
|
||||||
|
class="shadow-elevation-1 mt-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex w-full items-center justify-between py-2">
|
||||||
|
<div class="flex flex-col text-sm leading-normal font-medium">
|
||||||
|
<span>{{ log.taskName }}</span>
|
||||||
|
<span class="text-muted">
|
||||||
|
{{
|
||||||
|
isTaskInProgress(index)
|
||||||
|
? t('g.inProgress')
|
||||||
|
: t('g.completed') + ' ✓'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #toggleicon>
|
||||||
|
<Button
|
||||||
|
variant="textonly"
|
||||||
|
class="text-neutral-300"
|
||||||
|
@click="togglePanel(index)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
collapsedPanels[index]
|
||||||
|
? 'pi pi-chevron-right'
|
||||||
|
: 'pi pi-chevron-down'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
:ref="
|
||||||
|
index === focusedLogs.length - 1
|
||||||
|
? (el) => (lastPanelRef = el as HTMLElement)
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
class="h-64 overflow-y-auto rounded-lg bg-black"
|
||||||
|
:class="{
|
||||||
|
'h-64': index !== focusedLogs.length - 1,
|
||||||
|
grow: index === focusedLogs.length - 1
|
||||||
|
}"
|
||||||
|
@scroll="handleScroll"
|
||||||
|
>
|
||||||
|
<div class="h-full">
|
||||||
|
<div
|
||||||
|
v-for="(logLine, logIndex) in log.logs"
|
||||||
|
:key="logIndex"
|
||||||
|
class="text-muted"
|
||||||
|
>
|
||||||
|
<pre class="break-words whitespace-pre-wrap">{{
|
||||||
|
logLine
|
||||||
|
}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #footer="{ toggle }">
|
||||||
|
<div class="flex w-full items-center justify-between px-6 py-2 shadow-lg">
|
||||||
|
<div class="flex items-center text-base leading-none">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<template v-if="isInProgress">
|
||||||
|
<DotSpinner duration="1s" class="mr-2" />
|
||||||
|
<span>{{ currentTaskName }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isRestartCompleted">
|
||||||
|
<span class="mr-2">🎉</span>
|
||||||
|
<span>{{ currentTaskName }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="mr-2">✅</span>
|
||||||
|
<span>{{ t('manager.restartToApplyChanges') }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span v-if="isInProgress" class="text-sm text-muted-foreground">
|
||||||
|
{{ completedTasksCount }} {{ t('g.progressCountOf') }}
|
||||||
|
{{ totalTasksCount }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Button
|
||||||
|
v-if="!isInProgress && !isRestartCompleted"
|
||||||
|
variant="secondary"
|
||||||
|
class="mr-4 rounded-full border-2 border-base-foreground px-3 text-base-foreground hover:bg-secondary-background-hover"
|
||||||
|
@click="handleRestart"
|
||||||
|
>
|
||||||
|
{{ t('manager.applyChanges') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else-if="!isRestartCompleted"
|
||||||
|
variant="muted-textonly"
|
||||||
|
size="sm"
|
||||||
|
class="rounded-full font-bold"
|
||||||
|
:aria-label="
|
||||||
|
t(isExpanded ? 'contextMenu.Collapse' : 'contextMenu.Expand')
|
||||||
|
"
|
||||||
|
@click.stop="toggle"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="muted-textonly"
|
||||||
|
size="sm"
|
||||||
|
class="rounded-full font-bold"
|
||||||
|
:aria-label="t('g.close')"
|
||||||
|
@click.stop="closeToast"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoneyToast>
|
||||||
|
</template>
|
||||||
@@ -4,13 +4,6 @@ import { ref } from 'vue'
|
|||||||
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
|
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
|
||||||
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||||
|
|
||||||
// Mock dialog service
|
|
||||||
vi.mock('@/services/dialogService', () => ({
|
|
||||||
useDialogService: () => ({
|
|
||||||
showManagerProgressDialog: vi.fn()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock the app API
|
// Mock the app API
|
||||||
vi.mock('@/scripts/app', () => ({
|
vi.mock('@/scripts/app', () => ({
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { Ref } from 'vue'
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
|
||||||
import { normalizePackKeys } from '@/utils/packUtils'
|
import { normalizePackKeys } from '@/utils/packUtils'
|
||||||
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
import type { components } from '@/workbench/extensions/manager/types/generatedManagerTypes'
|
||||||
|
|
||||||
@@ -27,8 +26,6 @@ export const useManagerQueue = (
|
|||||||
taskQueue: Ref<ManagerTaskQueue>,
|
taskQueue: Ref<ManagerTaskQueue>,
|
||||||
installedPacks: Ref<Record<string, any>>
|
installedPacks: Ref<Record<string, any>>
|
||||||
) => {
|
) => {
|
||||||
const { showManagerProgressDialog } = useDialogService()
|
|
||||||
|
|
||||||
// Task queue state (read-only from server)
|
// Task queue state (read-only from server)
|
||||||
const maxHistoryItems = ref(64)
|
const maxHistoryItems = ref(64)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -113,15 +110,6 @@ export const useManagerQueue = (
|
|||||||
(event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
(event: CustomEvent<ManagerWsTaskDoneMsg>) => {
|
||||||
if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) {
|
if (event?.type === MANAGER_WS_TASK_DONE_NAME && event.detail?.state) {
|
||||||
updateTaskState(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<ManagerWsTaskStartedMsg>) => {
|
(event: CustomEvent<ManagerWsTaskStartedMsg>) => {
|
||||||
if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) {
|
if (event?.type === MANAGER_WS_TASK_STARTED_NAME && event.detail?.state) {
|
||||||
updateTaskState(event.detail.state)
|
updateTaskState(event.detail.state)
|
||||||
|
|
||||||
// Show progress dialog when a task starts
|
|
||||||
showManagerProgressDialog()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ vi.mock('@/workbench/extensions/manager/services/comfyManagerService', () => ({
|
|||||||
useComfyManagerService: vi.fn()
|
useComfyManagerService: vi.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/services/dialogService', () => ({
|
|
||||||
useDialogService: () => ({
|
|
||||||
showManagerProgressDialog: vi.fn()
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => {
|
vi.mock('@/workbench/extensions/manager/composables/useManagerQueue', () => {
|
||||||
const enqueueTaskMock = vi.fn()
|
const enqueueTaskMock = vi.fn()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useCachedRequest } from '@/composables/useCachedRequest'
|
|||||||
import { useServerLogs } from '@/composables/useServerLogs'
|
import { useServerLogs } from '@/composables/useServerLogs'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
|
||||||
import { normalizePackKeys } from '@/utils/packUtils'
|
import { normalizePackKeys } from '@/utils/packUtils'
|
||||||
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
|
import { useManagerQueue } from '@/workbench/extensions/manager/composables/useManagerQueue'
|
||||||
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
|
||||||
@@ -32,7 +32,6 @@ type UpdateAllPacksParams = components['schemas']['UpdateAllPacksParams']
|
|||||||
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const managerService = useComfyManagerService()
|
const managerService = useComfyManagerService()
|
||||||
const { showManagerProgressDialog } = useDialogService()
|
|
||||||
|
|
||||||
const installedPacks = ref<InstalledPacksResponse>({})
|
const installedPacks = ref<InstalledPacksResponse>({})
|
||||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||||
@@ -204,8 +203,6 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Show progress dialog immediately when task is queued
|
|
||||||
showManagerProgressDialog()
|
|
||||||
managerQueue.isProcessing.value = true
|
managerQueue.isProcessing.value = true
|
||||||
|
|
||||||
// Prepare logging hook
|
// Prepare logging hook
|
||||||
@@ -392,44 +389,3 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
|||||||
enablePack
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user