diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 9141e9135b..16f9b24a62 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -80,6 +80,12 @@ test.describe('Templates', () => { // Load a template await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() + + await comfyPage.page + .locator( + 'nav > div:nth-child(2) > div > span:has-text("Getting Started")' + ) + .click() await comfyPage.templates.loadTemplate('default') await expect(comfyPage.templates.content).toBeHidden() @@ -102,48 +108,72 @@ test.describe('Templates', () => { expect(await comfyPage.templates.content.isVisible()).toBe(true) }) - test('Uses title field as fallback when the key is not found in locales', async ({ + test('Uses proper locale files for templates', async ({ comfyPage }) => { + // Set locale to French before opening templates + await comfyPage.setSetting('Comfy.Locale', 'fr') + + // Load the templates dialog and wait for the French index file request + const requestPromise = comfyPage.page.waitForRequest( + '**/templates/index.fr.json' + ) + + await comfyPage.executeCommand('Comfy.BrowseTemplates') + + const request = await requestPromise + + // Verify French index was requested + expect(request.url()).toContain('templates/index.fr.json') + + await expect(comfyPage.templates.content).toBeVisible() + }) + + test('Falls back to English templates when locale file not found', async ({ comfyPage }) => { - // Capture request for the index.json - await comfyPage.page.route('**/templates/index.json', async (route, _) => { - // Add a new template that won't have a translation pre-generated - const response = [ - { - moduleName: 'default', - title: 'FALLBACK CATEGORY', - type: 'image', - templates: [ - { - name: 'unknown_key_has_no_translation_available', - title: 'FALLBACK TEMPLATE NAME', - mediaType: 'image', - mediaSubtype: 'webp', - description: 'No translations found' - } - ] - } - ] + // Set locale to a language that doesn't have a template file + await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists + + // Wait for the German request (expected to 404) + const germanRequestPromise = comfyPage.page.waitForRequest( + '**/templates/index.de.json' + ) + + // Wait for the fallback English request + const englishRequestPromise = comfyPage.page.waitForRequest( + '**/templates/index.json' + ) + + // Intercept the German file to simulate a 404 + await comfyPage.page.route('**/templates/index.de.json', async (route) => { await route.fulfill({ - status: 200, - body: JSON.stringify(response), - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store' - } + status: 404, + headers: { 'Content-Type': 'text/plain' }, + body: 'Not Found' }) }) + // Allow the English index to load normally + await comfyPage.page.route('**/templates/index.json', (route) => + route.continue() + ) + // Load the templates dialog await comfyPage.executeCommand('Comfy.BrowseTemplates') + await expect(comfyPage.templates.content).toBeVisible() + + // Verify German was requested first, then English as fallback + const germanRequest = await germanRequestPromise + const englishRequest = await englishRequestPromise + + expect(germanRequest.url()).toContain('templates/index.de.json') + expect(englishRequest.url()).toContain('templates/index.json') - // Expect the title to be used as fallback for template cards + // Verify English titles are shown as fallback await expect( - comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME') + comfyPage.templates.content.getByRole('heading', { + name: 'Image Generation' + }) ).toBeVisible() - - // Expect the title to be used as fallback for the template categories - await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible() }) test('template cards are dynamically sized and responsive', async ({ @@ -153,25 +183,43 @@ test.describe('Templates', () => { await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() - // Wait for at least one template card to appear - await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({ - timeout: 5000 - }) + const firstCard = comfyPage.page + .locator('[data-testid^="template-workflow-"]') + .first() + await expect(firstCard).toBeVisible({ timeout: 5000 }) - // Take snapshot of the template grid - const templateGrid = comfyPage.templates.content.locator('.grid').first() + // Get the template grid + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png') + + // Check grid layout at desktop size (default) + const desktopGridClass = await templateGrid.getAttribute('class') + expect(desktopGridClass).toContain('grid') + expect(desktopGridClass).toContain( + 'grid-cols-[repeat(auto-fill,minmax(16rem,1fr))]' + ) + + // Count visible cards at desktop size + const desktopCardCount = await comfyPage.page + .locator('[data-testid^="template-workflow-"]') + .count() + expect(desktopCardCount).toBeGreaterThan(0) // Check cards at mobile viewport size await comfyPage.page.setViewportSize({ width: 640, height: 800 }) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png') + // Grid should still be responsive at mobile size + const mobileGridClass = await templateGrid.getAttribute('class') + expect(mobileGridClass).toContain('grid') // Check cards at tablet size await comfyPage.page.setViewportSize({ width: 1024, height: 800 }) await expect(templateGrid).toBeVisible() - await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png') + // Grid should still be responsive at tablet size + const tabletGridClass = await templateGrid.getAttribute('class') + expect(tabletGridClass).toContain('grid') }) test('hover effects work on template cards', async ({ comfyPage }) => { @@ -179,10 +227,13 @@ test.describe('Templates', () => { await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() - // Get a template card - const firstCard = comfyPage.page.locator('.template-card').first() + // Get a template card using data-testid + const firstCard = comfyPage.page + .locator('[data-testid^="template-workflow-"]') + .first() await expect(firstCard).toBeVisible({ timeout: 5000 }) + // Check initial state - card should have transition classes // Take snapshot before hover await expect(firstCard).toHaveScreenshot('template-card-before-hover.png') @@ -257,21 +308,42 @@ test.describe('Templates', () => { await comfyPage.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() - // Verify cards are visible with varying content lengths - await expect( - comfyPage.page.getByText('This is a short description.') - ).toBeVisible({ timeout: 5000 }) + // Wait for cards to load await expect( - comfyPage.page.getByText('This is a medium length description') - ).toBeVisible({ timeout: 5000 }) - await expect( - comfyPage.page.getByText('This is a much longer description') + comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) ).toBeVisible({ timeout: 5000 }) - // Take snapshot of a grid with specific cards - const templateGrid = comfyPage.templates.content - .locator('.grid:has-text("Short Description")') - .first() + // Verify all three cards with different descriptions are visible + const shortDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-short-description"]' + ) + const mediumDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-medium-description"]' + ) + const longDescCard = comfyPage.page.locator( + '[data-testid="template-workflow-long-description"]' + ) + + await expect(shortDescCard).toBeVisible() + await expect(mediumDescCard).toBeVisible() + await expect(longDescCard).toBeVisible() + + // Verify descriptions are visible and have line-clamp class + // The description is in a p tag with text-muted class + const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2') + const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2') + const longDesc = longDescCard.locator('p.text-muted.line-clamp-2') + + await expect(shortDesc).toContainText('short description') + await expect(mediumDesc).toContainText('medium length description') + await expect(longDesc).toContainText('much longer description') + + // Verify grid layout maintains consistency + const templateGrid = comfyPage.page.locator( + '[data-testid="template-workflows-content"]' + ) await expect(templateGrid).toBeVisible() await expect(templateGrid).toHaveScreenshot( 'template-grid-varying-content.png' diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png index ce88325aad..6ca212261f 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-after-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png index bf1d18934d..3af64e7905 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-card-before-hover-chromium-linux.png differ diff --git a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png index 6c330c2f46..a3635bdaf9 100644 Binary files a/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png and b/browser_tests/tests/templates.spec.ts-snapshots/template-grid-varying-content-chromium-linux.png differ diff --git a/package.json b/package.json index ec94c881bb..9f0cc1ebd8 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@tiptap/extension-table-row": "^2.10.4", "@tiptap/starter-kit": "^2.10.4", "@vueuse/core": "^11.0.0", + "@vueuse/integrations": "^13.9.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-serialize": "^0.13.0", "@xterm/xterm": "^5.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ce1f4d341..dd30b30d92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@vueuse/core': specifier: ^11.0.0 version: 11.0.0(vue@3.5.13(typescript@5.9.2)) + '@vueuse/integrations': + specifier: ^13.9.0 + version: 13.9.0(axios@1.11.0)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.2)) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -2895,18 +2898,73 @@ packages: '@vueuse/core@12.8.2': resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/integrations@13.9.0': + resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + '@vueuse/metadata@11.0.0': resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==} '@vueuse/metadata@12.8.2': resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + '@vueuse/shared@11.0.0': resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==} '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + '@webgpu/types@0.1.51': resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==} @@ -6323,6 +6381,9 @@ packages: vue-component-type-helpers@3.0.7: resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==} + vue-component-type-helpers@3.0.8: + resolution: {integrity: sha512-WyR30Eq15Y/+odrUUMax6FmPbZwAp/HnC7qgR1r3lVFAcqwQ4wUoV79Mbh4SxDy3NiqDa+G4TOKD5xXSgBHo5A==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -8879,7 +8940,7 @@ snapshots: storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.2) - vue-component-type-helpers: 3.0.7 + vue-component-type-helpers: 3.0.8 '@swc/helpers@0.5.17': dependencies: @@ -9662,10 +9723,28 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/core@13.9.0(vue@3.5.13(typescript@5.9.2))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.2)) + vue: 3.5.13(typescript@5.9.2) + + '@vueuse/integrations@13.9.0(axios@1.11.0)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.2))': + dependencies: + '@vueuse/core': 13.9.0(vue@3.5.13(typescript@5.9.2)) + '@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.2)) + vue: 3.5.13(typescript@5.9.2) + optionalDependencies: + axios: 1.11.0 + fuse.js: 7.0.0 + '@vueuse/metadata@11.0.0': {} '@vueuse/metadata@12.8.2': {} + '@vueuse/metadata@13.9.0': {} + '@vueuse/shared@11.0.0(vue@3.5.13(typescript@5.9.2))': dependencies: vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.2)) @@ -9679,6 +9758,10 @@ snapshots: transitivePeerDependencies: - typescript + '@vueuse/shared@13.9.0(vue@3.5.13(typescript@5.9.2))': + dependencies: + vue: 3.5.13(typescript@5.9.2) + '@webgpu/types@0.1.51': {} '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': @@ -13545,6 +13628,8 @@ snapshots: vue-component-type-helpers@3.0.7: {} + vue-component-type-helpers@3.0.8: {} + vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)): dependencies: vue: 3.5.13(typescript@5.9.2) diff --git a/public/assets/images/default-template.png b/public/assets/images/default-template.png new file mode 100644 index 0000000000..8aeab89cc0 Binary files /dev/null and b/public/assets/images/default-template.png differ diff --git a/src/assets/icons/custom/dark-info.svg b/src/assets/icons/custom/dark-info.svg new file mode 100644 index 0000000000..26c05560f2 --- /dev/null +++ b/src/assets/icons/custom/dark-info.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/components/card/CardBottom.vue b/src/components/card/CardBottom.vue index 7f35754e6c..4a0ae10475 100644 --- a/src/components/card/CardBottom.vue +++ b/src/components/card/CardBottom.vue @@ -1,7 +1,19 @@ - + - + diff --git a/src/components/card/CardContainer.vue b/src/components/card/CardContainer.vue index 1a17d5659b..6b20db975a 100644 --- a/src/components/card/CardContainer.vue +++ b/src/components/card/CardContainer.vue @@ -1,5 +1,5 @@ - + @@ -8,20 +8,36 @@ diff --git a/src/components/card/CardTop.vue b/src/components/card/CardTop.vue index ed7e26089b..6470fa2ef5 100644 --- a/src/components/card/CardTop.vue +++ b/src/components/card/CardTop.vue @@ -14,7 +14,7 @@ - + diff --git a/src/components/chip/SquareChip.vue b/src/components/chip/SquareChip.vue index c886b089b2..06095aed0f 100644 --- a/src/components/chip/SquareChip.vue +++ b/src/components/chip/SquareChip.vue @@ -1,6 +1,6 @@ {{ label }} diff --git a/src/components/common/LazyImage.vue b/src/components/common/LazyImage.vue index 36aaa8a2b4..371968fba6 100644 --- a/src/components/common/LazyImage.vue +++ b/src/components/common/LazyImage.vue @@ -24,7 +24,13 @@ v-if="hasError" class="absolute inset-0 flex items-center justify-center bg-surface-50 dark-theme:bg-surface-800 text-muted" > - + diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue new file mode 100644 index 0000000000..d9f62f0b38 --- /dev/null +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -0,0 +1,739 @@ + + + + + + + + + {{ + $t('sideToolbar.templates', 'Templates') + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ pageTitle }} + + + + + + + + + + {{ $t('templateWorkflows.noResults', 'No templates found') }} + + + {{ + $t( + 'templateWorkflows.noResultsHint', + 'Try adjusting your search or filters' + ) + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ + getTemplateTitle( + template, + getEffectiveSourceModule(template) + ) + }} + + + {{ getTemplateDescription(template) }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('templateWorkflows.loadingMore', 'Loading more...') }} + + + + + + {{ + $t('templateWorkflows.resultsCount', { + count: filteredCount, + total: totalCount + }) + }} + + + + + + + + diff --git a/src/components/input/MultiSelect.vue b/src/components/input/MultiSelect.vue index d687c583b9..28111ff6ea 100644 --- a/src/components/input/MultiSelect.vue +++ b/src/components/input/MultiSelect.vue @@ -9,7 +9,7 @@ --> diff --git a/src/components/templates/TemplateWorkflowCard.spec.ts b/src/components/templates/TemplateWorkflowCard.spec.ts deleted file mode 100644 index 733b723d6c..0000000000 --- a/src/components/templates/TemplateWorkflowCard.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { mount } from '@vue/test-utils' -import { describe, expect, it, vi } from 'vitest' -import { ref } from 'vue' - -import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue' -import type { TemplateInfo } from '@/platform/workflow/templates/types/template' - -vi.mock('@/components/templates/thumbnails/AudioThumbnail.vue', () => ({ - default: { - name: 'AudioThumbnail', - template: '', - props: ['src'] - } -})) - -vi.mock('@/components/templates/thumbnails/CompareSliderThumbnail.vue', () => ({ - default: { - name: 'CompareSliderThumbnail', - template: - '', - props: ['baseImageSrc', 'overlayImageSrc', 'alt', 'isHovered'] - } -})) - -vi.mock('@/components/templates/thumbnails/DefaultThumbnail.vue', () => ({ - default: { - name: 'DefaultThumbnail', - template: '', - props: ['src', 'alt', 'isHovered', 'isVideo', 'hoverZoom'] - } -})) - -vi.mock('@/components/templates/thumbnails/HoverDissolveThumbnail.vue', () => ({ - default: { - name: 'HoverDissolveThumbnail', - template: - '', - props: ['baseImageSrc', 'overlayImageSrc', 'alt', 'isHovered'] - } -})) - -vi.mock('@vueuse/core', () => ({ - useElementHover: () => ref(false) -})) - -vi.mock('@/scripts/api', () => ({ - api: { - fileURL: (path: string) => `/fileURL${path}`, - apiURL: (path: string) => `/apiURL${path}`, - addEventListener: vi.fn(), - removeEventListener: vi.fn() - } -})) - -vi.mock('@/scripts/app', () => ({ - app: { - loadGraphData: vi.fn() - } -})) - -vi.mock('@/stores/dialogStore', () => ({ - useDialogStore: () => ({ - closeDialog: vi.fn() - }) -})) - -vi.mock( - '@/platform/workflow/templates/repositories/workflowTemplatesStore', - () => ({ - useWorkflowTemplatesStore: () => ({ - isLoaded: true, - loadWorkflowTemplates: vi.fn().mockResolvedValue(true), - groupedTemplates: [] - }) - }) -) - -vi.mock('vue-i18n', () => ({ - useI18n: () => ({ - t: (key: string, fallback: string) => fallback || key - }) -})) - -vi.mock( - '@/platform/workflow/templates/composables/useTemplateWorkflows', - () => ({ - useTemplateWorkflows: () => ({ - getTemplateThumbnailUrl: ( - template: TemplateInfo, - sourceModule: string, - index = '' - ) => { - const basePath = - sourceModule === 'default' - ? `/fileURL/templates/${template.name}` - : `/apiURL/workflow_templates/${sourceModule}/${template.name}` - const indexSuffix = - sourceModule === 'default' && index ? `-${index}` : '' - return `${basePath}${indexSuffix}.${template.mediaSubtype}` - }, - getTemplateTitle: (template: TemplateInfo, sourceModule: string) => { - const fallback = - template.title ?? template.name ?? `${sourceModule} Template` - return sourceModule === 'default' - ? template.localizedTitle ?? fallback - : fallback - }, - getTemplateDescription: ( - template: TemplateInfo, - sourceModule: string - ) => { - return sourceModule === 'default' - ? template.localizedDescription ?? '' - : template.description?.replace(/[-_]/g, ' ').trim() ?? '' - }, - loadWorkflowTemplate: vi.fn() - }) - }) -) - -describe('TemplateWorkflowCard', () => { - const createTemplate = (overrides = {}): TemplateInfo => ({ - name: 'test-template', - mediaType: 'image', - mediaSubtype: 'png', - thumbnailVariant: 'default', - description: 'Test description', - ...overrides - }) - - const mountCard = (props = {}) => { - return mount(TemplateWorkflowCard, { - props: { - sourceModule: 'default', - categoryTitle: 'Test Category', - loading: false, - template: createTemplate(), - ...props - }, - global: { - stubs: { - Card: { - template: - '', - props: ['dataTestid', 'pt'] - }, - ProgressSpinner: { - template: '' - } - } - } - }) - } - - it('emits loadWorkflow event when clicked', async () => { - const wrapper = mountCard({ - template: createTemplate({ name: 'test-workflow' }) - }) - await wrapper.find('.card').trigger('click') - expect(wrapper.emitted('loadWorkflow')).toBeTruthy() - expect(wrapper.emitted('loadWorkflow')?.[0]).toEqual(['test-workflow']) - }) - - it('shows loading spinner when loading is true', () => { - const wrapper = mountCard({ loading: true }) - expect(wrapper.find('.progress-spinner').exists()).toBe(true) - }) - - it('renders audio thumbnail for audio media type', () => { - const wrapper = mountCard({ - template: createTemplate({ mediaType: 'audio' }) - }) - expect(wrapper.find('.mock-audio-thumbnail').exists()).toBe(true) - }) - - it('renders compare slider thumbnail for compareSlider variant', () => { - const wrapper = mountCard({ - template: createTemplate({ thumbnailVariant: 'compareSlider' }) - }) - expect(wrapper.find('.mock-compare-slider').exists()).toBe(true) - }) - - it('renders hover dissolve thumbnail for hoverDissolve variant', () => { - const wrapper = mountCard({ - template: createTemplate({ thumbnailVariant: 'hoverDissolve' }) - }) - expect(wrapper.find('.mock-hover-dissolve').exists()).toBe(true) - }) - - it('renders default thumbnail by default', () => { - const wrapper = mountCard() - expect(wrapper.find('.mock-default-thumbnail').exists()).toBe(true) - }) - - it('passes correct props to default thumbnail for video', () => { - const wrapper = mountCard({ - template: createTemplate({ mediaType: 'video' }) - }) - const thumbnail = wrapper.find('.mock-default-thumbnail') - expect(thumbnail.exists()).toBe(true) - }) - - it('uses zoomHover scale when variant is zoomHover', () => { - const wrapper = mountCard({ - template: createTemplate({ thumbnailVariant: 'zoomHover' }) - }) - expect(wrapper.find('.mock-default-thumbnail').exists()).toBe(true) - }) - - it('displays localized title for default source module', () => { - const wrapper = mountCard({ - sourceModule: 'default', - template: createTemplate({ localizedTitle: 'My Localized Title' }) - }) - expect(wrapper.text()).toContain('My Localized Title') - }) - - it('displays template name as title for non-default source modules', () => { - const wrapper = mountCard({ - sourceModule: 'custom', - template: createTemplate({ name: 'custom-template' }) - }) - expect(wrapper.text()).toContain('custom-template') - }) - - it('displays localized description for default source module', () => { - const wrapper = mountCard({ - sourceModule: 'default', - template: createTemplate({ - localizedDescription: 'My Localized Description' - }) - }) - expect(wrapper.text()).toContain('My Localized Description') - }) - - it('processes description for non-default source modules', () => { - const wrapper = mountCard({ - sourceModule: 'custom', - template: createTemplate({ description: 'custom_module-description' }) - }) - expect(wrapper.text()).toContain('custom module description') - }) - - it('generates correct thumbnail URLs for default source module', () => { - const wrapper = mountCard({ - sourceModule: 'default', - template: createTemplate({ - name: 'my-template', - mediaSubtype: 'jpg' - }) - }) - const vm = wrapper.vm as any - expect(vm.baseThumbnailSrc).toBe('/fileURL/templates/my-template-1.jpg') - expect(vm.overlayThumbnailSrc).toBe('/fileURL/templates/my-template-2.jpg') - }) - - it('generates correct thumbnail URLs for custom source module', () => { - const wrapper = mountCard({ - sourceModule: 'custom-module', - template: createTemplate({ - name: 'my-template', - mediaSubtype: 'png' - }) - }) - const vm = wrapper.vm as any - expect(vm.baseThumbnailSrc).toBe( - '/apiURL/workflow_templates/custom-module/my-template.png' - ) - expect(vm.overlayThumbnailSrc).toBe( - '/apiURL/workflow_templates/custom-module/my-template.png' - ) - }) -}) diff --git a/src/components/templates/TemplateWorkflowCard.vue b/src/components/templates/TemplateWorkflowCard.vue deleted file mode 100644 index eb6a930388..0000000000 --- a/src/components/templates/TemplateWorkflowCard.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - {{ title }} - - - {{ description }} - - - - - - - - diff --git a/src/components/templates/TemplateWorkflowCardSkeleton.vue b/src/components/templates/TemplateWorkflowCardSkeleton.vue deleted file mode 100644 index 00bf738398..0000000000 --- a/src/components/templates/TemplateWorkflowCardSkeleton.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/templates/TemplateWorkflowList.vue b/src/components/templates/TemplateWorkflowList.vue deleted file mode 100644 index b6ac99c5ed..0000000000 --- a/src/components/templates/TemplateWorkflowList.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - - - {{ slotProps.data.title }} - - - - - - {{ slotProps.data.description }} - - - - - - - - - - - - diff --git a/src/components/templates/TemplateWorkflowView.spec.ts b/src/components/templates/TemplateWorkflowView.spec.ts deleted file mode 100644 index d9633a7e09..0000000000 --- a/src/components/templates/TemplateWorkflowView.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { mount } from '@vue/test-utils' -import { describe, expect, it, vi } from 'vitest' -import { createI18n } from 'vue-i18n' - -import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue' -import type { TemplateInfo } from '@/platform/workflow/templates/types/template' - -vi.mock('primevue/dataview', () => ({ - default: { - name: 'DataView', - template: ` - - - - - - - `, - props: ['value', 'layout', 'lazy', 'pt'] - } -})) - -vi.mock('primevue/selectbutton', () => ({ - default: { - name: 'SelectButton', - template: - '', - props: ['modelValue', 'options', 'allowEmpty'] - } -})) - -vi.mock('@/components/templates/TemplateWorkflowCard.vue', () => ({ - default: { - template: ` - - `, - props: ['sourceModule', 'categoryTitle', 'loading', 'template'], - emits: ['loadWorkflow'] - } -})) - -vi.mock('@/components/templates/TemplateWorkflowList.vue', () => ({ - default: { - template: '', - props: ['sourceModule', 'categoryTitle', 'loading', 'templates'], - emits: ['loadWorkflow'] - } -})) - -vi.mock('@/components/templates/TemplateSearchBar.vue', () => ({ - default: { - template: '', - props: ['searchQuery', 'filteredCount'], - emits: ['update:searchQuery', 'clearFilters'] - } -})) - -vi.mock('@/components/templates/TemplateWorkflowCardSkeleton.vue', () => ({ - default: { - template: '' - } -})) - -vi.mock('@vueuse/core', () => ({ - useLocalStorage: () => 'grid' -})) - -vi.mock('@/composables/useIntersectionObserver', () => ({ - useIntersectionObserver: vi.fn() -})) - -vi.mock('@/composables/useLazyPagination', () => ({ - useLazyPagination: (items: any) => ({ - paginatedItems: items, - isLoading: { value: false }, - hasMoreItems: { value: false }, - loadNextPage: vi.fn(), - reset: vi.fn() - }) -})) - -vi.mock('@/composables/useTemplateFiltering', () => ({ - useTemplateFiltering: (templates: any) => ({ - searchQuery: { value: '' }, - filteredTemplates: templates, - filteredCount: { value: templates.value?.length || 0 } - }) -})) - -describe('TemplateWorkflowView', () => { - const createTemplate = (name: string): TemplateInfo => ({ - name, - mediaType: 'image', - mediaSubtype: 'png', - thumbnailVariant: 'default', - description: `Description for ${name}` - }) - - const mountView = (props = {}) => { - const i18n = createI18n({ - legacy: false, - locale: 'en', - messages: { - en: { - templateWorkflows: { - loadingMore: 'Loading more...' - } - } - } - }) - - return mount(TemplateWorkflowView, { - props: { - title: 'Test Templates', - sourceModule: 'default', - categoryTitle: 'Test Category', - templates: [ - createTemplate('template-1'), - createTemplate('template-2'), - createTemplate('template-3') - ], - loading: null, - ...props - }, - global: { - plugins: [i18n] - } - }) - } - - it('renders template cards for each template', () => { - const wrapper = mountView() - const cards = wrapper.findAll('.mock-template-card') - - expect(cards.length).toBe(3) - expect(cards[0].attributes('data-name')).toBe('template-1') - expect(cards[1].attributes('data-name')).toBe('template-2') - expect(cards[2].attributes('data-name')).toBe('template-3') - }) - - it('emits loadWorkflow event when clicked', async () => { - const wrapper = mountView() - const card = wrapper.find('.mock-template-card') - - await card.trigger('click') - - expect(wrapper.emitted()).toHaveProperty('loadWorkflow') - // Check that the emitted event contains the template name - const emitted = wrapper.emitted('loadWorkflow') - expect(emitted).toBeTruthy() - expect(emitted?.[0][0]).toBe('template-1') - }) - - it('passes correct props to template cards', () => { - const wrapper = mountView({ - sourceModule: 'custom', - categoryTitle: 'Custom Category' - }) - - const card = wrapper.find('.mock-template-card') - expect(card.exists()).toBe(true) - expect(card.attributes('data-source-module')).toBe('custom') - expect(card.attributes('data-category-title')).toBe('Custom Category') - }) - - it('applies loading state correctly to cards', () => { - const wrapper = mountView({ - loading: 'template-2' - }) - - const cards = wrapper.findAll('.mock-template-card') - - // Only the second card should have loading=true since loading="template-2" - expect(cards[0].attributes('data-loading')).toBe('false') - expect(cards[1].attributes('data-loading')).toBe('true') - expect(cards[2].attributes('data-loading')).toBe('false') - }) -}) diff --git a/src/components/templates/TemplateWorkflowView.vue b/src/components/templates/TemplateWorkflowView.vue deleted file mode 100644 index 9c73ddc86c..0000000000 --- a/src/components/templates/TemplateWorkflowView.vue +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - {{ title }} - - - - - - - reset()" - /> - - - - - - - - - - - - - - - - {{ t('templateWorkflows.loadingMore') }} - - - - - - - - diff --git a/src/components/templates/TemplateWorkflowsContent.vue b/src/components/templates/TemplateWorkflowsContent.vue deleted file mode 100644 index 38925b9e40..0000000000 --- a/src/components/templates/TemplateWorkflowsContent.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - diff --git a/src/components/templates/TemplateWorkflowsDialogHeader.vue b/src/components/templates/TemplateWorkflowsDialogHeader.vue deleted file mode 100644 index 9313ab104d..0000000000 --- a/src/components/templates/TemplateWorkflowsDialogHeader.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - - {{ $t('templateWorkflows.title') }} - - - diff --git a/src/components/templates/TemplateWorkflowsSideNav.vue b/src/components/templates/TemplateWorkflowsSideNav.vue deleted file mode 100644 index 07ff87990e..0000000000 --- a/src/components/templates/TemplateWorkflowsSideNav.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - {{ slotProps.option.label }} - - - - - - - - diff --git a/src/components/templates/thumbnails/AudioThumbnail.vue b/src/components/templates/thumbnails/AudioThumbnail.vue index dda6e79a52..49333d93e2 100644 --- a/src/components/templates/thumbnails/AudioThumbnail.vue +++ b/src/components/templates/thumbnails/AudioThumbnail.vue @@ -1,6 +1,12 @@ - + diff --git a/src/components/templates/thumbnails/BaseThumbnail.spec.ts b/src/components/templates/thumbnails/BaseThumbnail.spec.ts index cd0b44f535..ecb03df419 100644 --- a/src/components/templates/thumbnails/BaseThumbnail.spec.ts +++ b/src/components/templates/thumbnails/BaseThumbnail.spec.ts @@ -51,8 +51,9 @@ describe('BaseThumbnail', () => { vm.error = true await nextTick() - expect(wrapper.find('.pi-file').exists()).toBe(true) - expect(wrapper.find('.transform-gpu').exists()).toBe(false) + expect( + wrapper.find('img[src="/assets/images/default-template.png"]').exists() + ).toBe(true) }) it('applies transition classes to content', () => { diff --git a/src/components/templates/thumbnails/BaseThumbnail.vue b/src/components/templates/thumbnails/BaseThumbnail.vue index ee4c0f3a4d..62168a561b 100644 --- a/src/components/templates/thumbnails/BaseThumbnail.vue +++ b/src/components/templates/thumbnails/BaseThumbnail.vue @@ -1,5 +1,7 @@ - + - + diff --git a/src/components/widget/nav/NavIcon.vue b/src/components/widget/nav/NavIcon.vue index 653a6a70ed..bcc5c5a00d 100644 --- a/src/components/widget/nav/NavIcon.vue +++ b/src/components/widget/nav/NavIcon.vue @@ -1,5 +1,5 @@ - + diff --git a/src/components/widget/panel/LeftSidePanel.vue b/src/components/widget/panel/LeftSidePanel.vue index 964753365d..122644789e 100644 --- a/src/components/widget/panel/LeftSidePanel.vue +++ b/src/components/widget/panel/LeftSidePanel.vue @@ -1,5 +1,7 @@ - + @@ -10,16 +12,22 @@ - - - {{ subItem.label }} - + + + + {{ subItem.label }} + +
+ {{ $t('templateWorkflows.noResults', 'No templates found') }} +
+ {{ + $t( + 'templateWorkflows.noResultsHint', + 'Try adjusting your search or filters' + ) + }} +
+ {{ getTemplateDescription(template) }} +
- {{ description }} -