[Feature] Add "All" category to template workflows (#3931)
Co-authored-by: github-actions <github-actions@github.com>
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 78 KiB |
@@ -46,10 +46,68 @@ vi.mock('@vueuse/core', () => ({
|
|||||||
vi.mock('@/scripts/api', () => ({
|
vi.mock('@/scripts/api', () => ({
|
||||||
api: {
|
api: {
|
||||||
fileURL: (path: string) => `/fileURL${path}`,
|
fileURL: (path: string) => `/fileURL${path}`,
|
||||||
apiURL: (path: string) => `/apiURL${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('@/stores/workflowTemplatesStore', () => ({
|
||||||
|
useWorkflowTemplatesStore: () => ({
|
||||||
|
isLoaded: true,
|
||||||
|
loadWorkflowTemplates: vi.fn().mockResolvedValue(true),
|
||||||
|
groupedTemplates: []
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
t: (key: string, fallback: string) => fallback || key
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/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', () => {
|
describe('TemplateWorkflowCard', () => {
|
||||||
const createTemplate = (overrides = {}): TemplateInfo => ({
|
const createTemplate = (overrides = {}): TemplateInfo => ({
|
||||||
name: 'test-template',
|
name: 'test-template',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
|
|||||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||||
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
|
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
|
||||||
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
|
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
|
||||||
import { api } from '@/scripts/api'
|
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
|
||||||
import { TemplateInfo } from '@/types/workflowTemplateTypes'
|
import { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||||
|
|
||||||
const UPSCALE_ZOOM_SCALE = 16 // for upscale templates, exaggerate the hover zoom
|
const UPSCALE_ZOOM_SCALE = 16 // for upscale templates, exaggerate the hover zoom
|
||||||
@@ -102,36 +102,36 @@ const { sourceModule, loading, template } = defineProps<{
|
|||||||
const cardRef = ref<HTMLElement | null>(null)
|
const cardRef = ref<HTMLElement | null>(null)
|
||||||
const isHovered = useElementHover(cardRef)
|
const isHovered = useElementHover(cardRef)
|
||||||
|
|
||||||
const getThumbnailUrl = (index = '') => {
|
const { getTemplateThumbnailUrl, getTemplateTitle, getTemplateDescription } =
|
||||||
const basePath =
|
useTemplateWorkflows()
|
||||||
sourceModule === 'default'
|
|
||||||
? api.fileURL(`/templates/${template.name}`)
|
|
||||||
: api.apiURL(`/workflow_templates/${sourceModule}/${template.name}`)
|
|
||||||
|
|
||||||
// For templates from custom nodes, multiple images is not yet supported
|
// Determine the effective source module to use (from template or prop)
|
||||||
const indexSuffix = sourceModule === 'default' && index ? `-${index}` : ''
|
const effectiveSourceModule = computed(
|
||||||
|
() => template.sourceModule || sourceModule
|
||||||
return `${basePath}${indexSuffix}.${template.mediaSubtype}`
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const baseThumbnailSrc = computed(() =>
|
const baseThumbnailSrc = computed(() =>
|
||||||
getThumbnailUrl(sourceModule === 'default' ? '1' : '')
|
getTemplateThumbnailUrl(
|
||||||
|
template,
|
||||||
|
effectiveSourceModule.value,
|
||||||
|
effectiveSourceModule.value === 'default' ? '1' : ''
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const overlayThumbnailSrc = computed(() =>
|
const overlayThumbnailSrc = computed(() =>
|
||||||
getThumbnailUrl(sourceModule === 'default' ? '2' : '')
|
getTemplateThumbnailUrl(
|
||||||
|
template,
|
||||||
|
effectiveSourceModule.value,
|
||||||
|
effectiveSourceModule.value === 'default' ? '2' : ''
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const description = computed(() => {
|
const description = computed(() =>
|
||||||
return sourceModule === 'default'
|
getTemplateDescription(template, effectiveSourceModule.value)
|
||||||
? template.localizedDescription ?? ''
|
)
|
||||||
: template.description.replace(/[-_]/g, ' ').trim()
|
const title = computed(() =>
|
||||||
})
|
getTemplateTitle(template, effectiveSourceModule.value)
|
||||||
|
)
|
||||||
const title = computed(() => {
|
|
||||||
return sourceModule === 'default'
|
|
||||||
? template.localizedTitle ?? ''
|
|
||||||
: template.name
|
|
||||||
})
|
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
loadWorkflow: [name: string]
|
loadWorkflow: [name: string]
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<DataTable
|
<DataTable
|
||||||
v-model:selection="selectedTemplate"
|
v-model:selection="selectedTemplate"
|
||||||
:value="templates"
|
:value="enrichedTemplates"
|
||||||
striped-rows
|
striped-rows
|
||||||
selection-mode="single"
|
selection-mode="single"
|
||||||
>
|
>
|
||||||
<Column field="title" :header="$t('g.title')">
|
<Column field="title" :header="$t('g.title')">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<span :title="getTemplateTitle(slotProps.data)">{{
|
<span :title="slotProps.data.title">{{ slotProps.data.title }}</span>
|
||||||
getTemplateTitle(slotProps.data)
|
|
||||||
}}</span>
|
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column field="description" :header="$t('g.description')">
|
<Column field="description" :header="$t('g.description')">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<span :title="getTemplateDescription(slotProps.data)">
|
<span :title="slotProps.data.description">
|
||||||
{{ getTemplateDescription(slotProps.data) }}
|
{{ slotProps.data.description }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@@ -38,8 +36,9 @@
|
|||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Column from 'primevue/column'
|
import Column from 'primevue/column'
|
||||||
import DataTable from 'primevue/datatable'
|
import DataTable from 'primevue/datatable'
|
||||||
import { ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
|
||||||
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
|
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
|
||||||
|
|
||||||
const { sourceModule, loading, templates } = defineProps<{
|
const { sourceModule, loading, templates } = defineProps<{
|
||||||
@@ -50,21 +49,20 @@ const { sourceModule, loading, templates } = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const selectedTemplate = ref(null)
|
const selectedTemplate = ref(null)
|
||||||
|
const { getTemplateTitle, getTemplateDescription } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
const enrichedTemplates = computed(() => {
|
||||||
|
return templates.map((template) => {
|
||||||
|
const actualSourceModule = template.sourceModule || sourceModule
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
title: getTemplateTitle(template, actualSourceModule),
|
||||||
|
description: getTemplateDescription(template, actualSourceModule)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
loadWorkflow: [name: string]
|
loadWorkflow: [name: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const getTemplateTitle = (template: TemplateInfo) => {
|
|
||||||
const fallback = template.title ?? template.name ?? `${sourceModule} Template`
|
|
||||||
return sourceModule === 'default'
|
|
||||||
? template.localizedTitle ?? fallback
|
|
||||||
: fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTemplateDescription = (template: TemplateInfo) => {
|
|
||||||
return sourceModule === 'default'
|
|
||||||
? template.localizedDescription ?? ''
|
|
||||||
: template.description.replace(/[-_]/g, ' ').trim()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
class="absolute translate-x-0 top-0 left-0 h-full w-80 shadow-md z-5 transition-transform duration-300 ease-in-out"
|
class="absolute translate-x-0 top-0 left-0 h-full w-80 shadow-md z-5 transition-transform duration-300 ease-in-out"
|
||||||
>
|
>
|
||||||
<ProgressSpinner
|
<ProgressSpinner
|
||||||
v-if="!workflowTemplatesStore.isLoaded || !isReady"
|
v-if="!isTemplatesLoaded || !isReady"
|
||||||
class="absolute w-8 h-full inset-0"
|
class="absolute w-8 h-full inset-0"
|
||||||
/>
|
/>
|
||||||
<TemplateWorkflowsSideNav
|
<TemplateWorkflowsSideNav
|
||||||
:tabs="tabs"
|
:tabs="allTemplateGroups"
|
||||||
:selected-tab="selectedTab"
|
:selected-tab="selectedTemplate"
|
||||||
@update:selected-tab="handleTabSelection"
|
@update:selected-tab="handleTabSelection"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -37,14 +37,14 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<TemplateWorkflowView
|
<TemplateWorkflowView
|
||||||
v-if="isReady && selectedTab"
|
v-if="isReady && selectedTemplate"
|
||||||
class="px-12 py-4"
|
class="px-12 py-4"
|
||||||
:title="selectedTab.title"
|
:title="selectedTemplate.title"
|
||||||
:source-module="selectedTab.moduleName"
|
:source-module="selectedTemplate.moduleName"
|
||||||
:templates="selectedTab.templates"
|
:templates="selectedTemplate.templates"
|
||||||
:loading="workflowLoading"
|
:loading="loadingTemplateId"
|
||||||
:category-title="selectedTab.title"
|
:category-title="selectedTemplate.title"
|
||||||
@load-workflow="loadWorkflow"
|
@load-workflow="handleLoadWorkflow"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,47 +56,46 @@ import { useAsyncState } from '@vueuse/core'
|
|||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Divider from 'primevue/divider'
|
import Divider from 'primevue/divider'
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import ProgressSpinner from 'primevue/progressspinner'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
|
import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
|
||||||
import TemplateWorkflowsSideNav from '@/components/templates/TemplateWorkflowsSideNav.vue'
|
import TemplateWorkflowsSideNav from '@/components/templates/TemplateWorkflowsSideNav.vue'
|
||||||
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
|
||||||
import { api } from '@/scripts/api'
|
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
|
||||||
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
|
|
||||||
import type { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
import type { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isOpen: isSideNavOpen,
|
isOpen: isSideNavOpen,
|
||||||
toggle: toggleSideNav
|
toggle: toggleSideNav
|
||||||
} = useResponsiveCollapse()
|
} = useResponsiveCollapse()
|
||||||
|
|
||||||
const workflowTemplatesStore = useWorkflowTemplatesStore()
|
const {
|
||||||
const { isReady } = useAsyncState(
|
selectedTemplate,
|
||||||
workflowTemplatesStore.loadWorkflowTemplates,
|
loadingTemplateId,
|
||||||
null
|
isTemplatesLoaded,
|
||||||
|
allTemplateGroups,
|
||||||
|
loadTemplates,
|
||||||
|
selectFirstTemplateCategory,
|
||||||
|
selectTemplateCategory,
|
||||||
|
loadWorkflowTemplate
|
||||||
|
} = useTemplateWorkflows()
|
||||||
|
|
||||||
|
const { isReady } = useAsyncState(loadTemplates, null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
isReady,
|
||||||
|
() => {
|
||||||
|
if (isReady.value) {
|
||||||
|
selectFirstTemplateCategory()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectedTab = ref<WorkflowTemplates | null>(null)
|
|
||||||
const selectFirstTab = () => {
|
|
||||||
const firstTab = workflowTemplatesStore.groupedTemplates[0].modules[0]
|
|
||||||
handleTabSelection(firstTab)
|
|
||||||
}
|
|
||||||
watch(isReady, selectFirstTab, { once: true })
|
|
||||||
|
|
||||||
const workflowLoading = ref<string | null>(null)
|
|
||||||
|
|
||||||
const tabs = computed(() => workflowTemplatesStore.groupedTemplates)
|
|
||||||
|
|
||||||
const handleTabSelection = (selection: WorkflowTemplates | null) => {
|
const handleTabSelection = (selection: WorkflowTemplates | null) => {
|
||||||
//Listbox allows deselecting so this special case is ignored here
|
if (selection !== null) {
|
||||||
if (selection !== selectedTab.value && selection !== null) {
|
selectTemplateCategory(selection)
|
||||||
selectedTab.value = selection
|
|
||||||
|
|
||||||
// On small screens, close the sidebar when a category is selected
|
// On small screens, close the sidebar when a category is selected
|
||||||
if (isSmallScreen.value) {
|
if (isSmallScreen.value) {
|
||||||
@@ -105,30 +104,9 @@ const handleTabSelection = (selection: WorkflowTemplates | null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadWorkflow = async (id: string) => {
|
const handleLoadWorkflow = async (id: string) => {
|
||||||
if (!isReady.value) return
|
if (!isReady.value || !selectedTemplate.value) return false
|
||||||
|
|
||||||
workflowLoading.value = id
|
return loadWorkflowTemplate(id, selectedTemplate.value.moduleName)
|
||||||
let json
|
|
||||||
if (selectedTab.value?.moduleName === 'default') {
|
|
||||||
// Default templates provided by frontend are served on this separate endpoint
|
|
||||||
json = await fetch(api.fileURL(`/templates/${id}.json`)).then((r) =>
|
|
||||||
r.json()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
json = await fetch(
|
|
||||||
api.apiURL(
|
|
||||||
`/workflow_templates/${selectedTab.value?.moduleName}/${id}.json`
|
|
||||||
)
|
|
||||||
).then((r) => r.json())
|
|
||||||
}
|
|
||||||
useDialogStore().closeDialog()
|
|
||||||
const workflowName =
|
|
||||||
selectedTab.value?.moduleName === 'default'
|
|
||||||
? t(`templateWorkflows.template.${id}`, id)
|
|
||||||
: id
|
|
||||||
await app.loadGraphData(json, true, true, workflowName)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ Composables for sidebar functionality:
|
|||||||
- `useNodeLibrarySidebarTab` - Manages the node library sidebar tab
|
- `useNodeLibrarySidebarTab` - Manages the node library sidebar tab
|
||||||
- `useQueueSidebarTab` - Manages the queue sidebar tab
|
- `useQueueSidebarTab` - Manages the queue sidebar tab
|
||||||
- `useWorkflowsSidebarTab` - Manages the workflows sidebar tab
|
- `useWorkflowsSidebarTab` - Manages the workflows sidebar tab
|
||||||
|
- `useTemplateWorkflows` - Manages template workflow loading, selection, and display
|
||||||
|
|
||||||
### Widgets
|
### Widgets
|
||||||
|
|
||||||
|
|||||||
190
src/composables/useTemplateWorkflows.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
|
||||||
|
import type {
|
||||||
|
TemplateGroup,
|
||||||
|
TemplateInfo,
|
||||||
|
WorkflowTemplates
|
||||||
|
} from '@/types/workflowTemplateTypes'
|
||||||
|
|
||||||
|
export function useTemplateWorkflows() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const workflowTemplatesStore = useWorkflowTemplatesStore()
|
||||||
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
// State
|
||||||
|
const selectedTemplate = ref<WorkflowTemplates | null>(null)
|
||||||
|
const loadingTemplateId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const isTemplatesLoaded = computed(() => workflowTemplatesStore.isLoaded)
|
||||||
|
const allTemplateGroups = computed<TemplateGroup[]>(
|
||||||
|
() => workflowTemplatesStore.groupedTemplates
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all template workflows from the API
|
||||||
|
*/
|
||||||
|
const loadTemplates = async () => {
|
||||||
|
if (!workflowTemplatesStore.isLoaded) {
|
||||||
|
await workflowTemplatesStore.loadWorkflowTemplates()
|
||||||
|
}
|
||||||
|
return workflowTemplatesStore.isLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the first template category as default
|
||||||
|
*/
|
||||||
|
const selectFirstTemplateCategory = () => {
|
||||||
|
if (allTemplateGroups.value.length > 0) {
|
||||||
|
const firstCategory = allTemplateGroups.value[0].modules[0]
|
||||||
|
selectTemplateCategory(firstCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a template category
|
||||||
|
*/
|
||||||
|
const selectTemplateCategory = (category: WorkflowTemplates | null) => {
|
||||||
|
selectedTemplate.value = category
|
||||||
|
return category !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets template thumbnail URL
|
||||||
|
*/
|
||||||
|
const getTemplateThumbnailUrl = (
|
||||||
|
template: TemplateInfo,
|
||||||
|
sourceModule: string,
|
||||||
|
index = ''
|
||||||
|
) => {
|
||||||
|
const basePath =
|
||||||
|
sourceModule === 'default'
|
||||||
|
? api.fileURL(`/templates/${template.name}`)
|
||||||
|
: api.apiURL(`/workflow_templates/${sourceModule}/${template.name}`)
|
||||||
|
|
||||||
|
const indexSuffix = sourceModule === 'default' && index ? `-${index}` : ''
|
||||||
|
return `${basePath}${indexSuffix}.${template.mediaSubtype}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets formatted template title
|
||||||
|
*/
|
||||||
|
const getTemplateTitle = (template: TemplateInfo, sourceModule: string) => {
|
||||||
|
const fallback =
|
||||||
|
template.title ?? template.name ?? `${sourceModule} Template`
|
||||||
|
return sourceModule === 'default'
|
||||||
|
? template.localizedTitle ?? fallback
|
||||||
|
: fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets formatted template description
|
||||||
|
*/
|
||||||
|
const getTemplateDescription = (
|
||||||
|
template: TemplateInfo,
|
||||||
|
sourceModule: string
|
||||||
|
) => {
|
||||||
|
return sourceModule === 'default'
|
||||||
|
? template.localizedDescription ?? ''
|
||||||
|
: template.description?.replace(/[-_]/g, ' ').trim() ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a workflow template
|
||||||
|
*/
|
||||||
|
const loadWorkflowTemplate = async (id: string, sourceModule: string) => {
|
||||||
|
if (!isTemplatesLoaded.value) return false
|
||||||
|
|
||||||
|
loadingTemplateId.value = id
|
||||||
|
let json
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Handle "All" category as a special case
|
||||||
|
if (sourceModule === 'all') {
|
||||||
|
// Find "All" category in the ComfyUI Examples group
|
||||||
|
const comfyExamplesGroup = allTemplateGroups.value.find(
|
||||||
|
(g) =>
|
||||||
|
g.label ===
|
||||||
|
t('templateWorkflows.category.ComfyUI Examples', 'ComfyUI Examples')
|
||||||
|
)
|
||||||
|
const allCategory = comfyExamplesGroup?.modules.find(
|
||||||
|
(m) => m.moduleName === 'all'
|
||||||
|
)
|
||||||
|
const template = allCategory?.templates.find((t) => t.name === id)
|
||||||
|
|
||||||
|
if (!template || !template.sourceModule) return false
|
||||||
|
|
||||||
|
// Use the stored source module for loading
|
||||||
|
const actualSourceModule = template.sourceModule
|
||||||
|
json = await fetchTemplateJson(id, actualSourceModule)
|
||||||
|
|
||||||
|
// Use source module for name
|
||||||
|
const workflowName =
|
||||||
|
actualSourceModule === 'default'
|
||||||
|
? t(`templateWorkflows.template.${id}`, id)
|
||||||
|
: id
|
||||||
|
|
||||||
|
dialogStore.closeDialog()
|
||||||
|
await app.loadGraphData(json, true, true, workflowName)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular case for normal categories
|
||||||
|
json = await fetchTemplateJson(id, sourceModule)
|
||||||
|
|
||||||
|
const workflowName =
|
||||||
|
sourceModule === 'default'
|
||||||
|
? t(`templateWorkflows.template.${id}`, id)
|
||||||
|
: id
|
||||||
|
|
||||||
|
dialogStore.closeDialog()
|
||||||
|
await app.loadGraphData(json, true, true, workflowName)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading workflow template:', error)
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
loadingTemplateId.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches template JSON from the appropriate endpoint
|
||||||
|
*/
|
||||||
|
const fetchTemplateJson = async (id: string, sourceModule: string) => {
|
||||||
|
if (sourceModule === 'default') {
|
||||||
|
// Default templates provided by frontend are served on this separate endpoint
|
||||||
|
return fetch(api.fileURL(`/templates/${id}.json`)).then((r) => r.json())
|
||||||
|
} else {
|
||||||
|
return fetch(
|
||||||
|
api.apiURL(`/workflow_templates/${sourceModule}/${id}.json`)
|
||||||
|
).then((r) => r.json())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
selectedTemplate,
|
||||||
|
loadingTemplateId,
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
isTemplatesLoaded,
|
||||||
|
allTemplateGroups,
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
loadTemplates,
|
||||||
|
selectFirstTemplateCategory,
|
||||||
|
selectTemplateCategory,
|
||||||
|
getTemplateThumbnailUrl,
|
||||||
|
getTemplateTitle,
|
||||||
|
getTemplateDescription,
|
||||||
|
loadWorkflowTemplate
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -514,7 +514,8 @@
|
|||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
"Audio": "Audio",
|
"Audio": "Audio",
|
||||||
"Image API": "Image API",
|
"Image API": "Image API",
|
||||||
"Video API": "Video API"
|
"Video API": "Video API",
|
||||||
|
"All": "All Templates"
|
||||||
},
|
},
|
||||||
"templateDescription": {
|
"templateDescription": {
|
||||||
"Basics": {
|
"Basics": {
|
||||||
|
|||||||
@@ -1138,6 +1138,7 @@
|
|||||||
"templateWorkflows": {
|
"templateWorkflows": {
|
||||||
"category": {
|
"category": {
|
||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
|
"All": "Todas las plantillas",
|
||||||
"Area Composition": "Composición de Área",
|
"Area Composition": "Composición de Área",
|
||||||
"Audio": "Audio",
|
"Audio": "Audio",
|
||||||
"Basics": "Básicos",
|
"Basics": "Básicos",
|
||||||
|
|||||||
@@ -1138,6 +1138,7 @@
|
|||||||
"templateWorkflows": {
|
"templateWorkflows": {
|
||||||
"category": {
|
"category": {
|
||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
|
"All": "Tous les modèles",
|
||||||
"Area Composition": "Composition de zone",
|
"Area Composition": "Composition de zone",
|
||||||
"Audio": "Audio",
|
"Audio": "Audio",
|
||||||
"Basics": "Basiques",
|
"Basics": "Basiques",
|
||||||
|
|||||||
@@ -1138,6 +1138,7 @@
|
|||||||
"templateWorkflows": {
|
"templateWorkflows": {
|
||||||
"category": {
|
"category": {
|
||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
|
"All": "すべてのテンプレート",
|
||||||
"Area Composition": "エリア構成",
|
"Area Composition": "エリア構成",
|
||||||
"Audio": "オーディオ",
|
"Audio": "オーディオ",
|
||||||
"Basics": "基本",
|
"Basics": "基本",
|
||||||
|
|||||||
@@ -1138,6 +1138,7 @@
|
|||||||
"templateWorkflows": {
|
"templateWorkflows": {
|
||||||
"category": {
|
"category": {
|
||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
|
"All": "모든 템플릿",
|
||||||
"Area Composition": "영역 구성",
|
"Area Composition": "영역 구성",
|
||||||
"Audio": "오디오",
|
"Audio": "오디오",
|
||||||
"Basics": "기본",
|
"Basics": "기본",
|
||||||
|
|||||||
@@ -1138,6 +1138,7 @@
|
|||||||
"templateWorkflows": {
|
"templateWorkflows": {
|
||||||
"category": {
|
"category": {
|
||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
|
"All": "Все шаблоны",
|
||||||
"Area Composition": "Композиция области",
|
"Area Composition": "Композиция области",
|
||||||
"Audio": "Аудио",
|
"Audio": "Аудио",
|
||||||
"Basics": "Основы",
|
"Basics": "Основы",
|
||||||
|
|||||||
@@ -1138,6 +1138,7 @@
|
|||||||
"templateWorkflows": {
|
"templateWorkflows": {
|
||||||
"category": {
|
"category": {
|
||||||
"3D": "3D",
|
"3D": "3D",
|
||||||
|
"All": "所有模板",
|
||||||
"Area Composition": "区域组成",
|
"Area Composition": "区域组成",
|
||||||
"Audio": "音频",
|
"Audio": "音频",
|
||||||
"Basics": "基础",
|
"Basics": "基础",
|
||||||
|
|||||||
@@ -101,7 +101,51 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Create an "All" category that combines all templates
|
||||||
|
const createAllCategory = () => {
|
||||||
|
// First, get core templates with source module added
|
||||||
|
const coreTemplatesWithSourceModule = coreTemplates.value.flatMap(
|
||||||
|
(category) =>
|
||||||
|
// For each template in each category, add the sourceModule and pass through any localized fields
|
||||||
|
category.templates.map((template) => {
|
||||||
|
// Get localized template with its original category title for i18n lookup
|
||||||
|
const localizedTemplate = addLocalizedFieldsToTemplate(
|
||||||
|
template,
|
||||||
|
category.title
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...localizedTemplate,
|
||||||
|
sourceModule: category.moduleName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now handle custom templates
|
||||||
|
const customTemplatesWithSourceModule = Object.entries(
|
||||||
|
customTemplates.value
|
||||||
|
).flatMap(([moduleName, templates]) =>
|
||||||
|
templates.map((name) => ({
|
||||||
|
name,
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
description: name,
|
||||||
|
sourceModule: moduleName
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
moduleName: 'all',
|
||||||
|
title: 'All',
|
||||||
|
localizedTitle: st('templateWorkflows.category.All', 'All Templates'),
|
||||||
|
templates: [
|
||||||
|
...coreTemplatesWithSourceModule,
|
||||||
|
...customTemplatesWithSourceModule
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const groupedTemplates = computed<TemplateGroup[]>(() => {
|
const groupedTemplates = computed<TemplateGroup[]>(() => {
|
||||||
|
// Get regular categories
|
||||||
const allTemplates = [
|
const allTemplates = [
|
||||||
...sortCategoryTemplates(coreTemplates.value).map(
|
...sortCategoryTemplates(coreTemplates.value).map(
|
||||||
localizeTemplateCategory
|
localizeTemplateCategory
|
||||||
@@ -124,7 +168,8 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
return Object.entries(
|
// Group templates by their main category
|
||||||
|
const groupedByCategory = Object.entries(
|
||||||
groupBy(allTemplates, (template) =>
|
groupBy(allTemplates, (template) =>
|
||||||
template.moduleName === 'default'
|
template.moduleName === 'default'
|
||||||
? st(
|
? st(
|
||||||
@@ -134,6 +179,21 @@ export const useWorkflowTemplatesStore = defineStore(
|
|||||||
: st('templateWorkflows.category.Custom Nodes', 'Custom Nodes')
|
: st('templateWorkflows.category.Custom Nodes', 'Custom Nodes')
|
||||||
)
|
)
|
||||||
).map(([label, modules]) => ({ label, modules }))
|
).map(([label, modules]) => ({ label, modules }))
|
||||||
|
|
||||||
|
// Insert the "All" category at the top of the "ComfyUI Examples" group
|
||||||
|
const comfyExamplesGroupIndex = groupedByCategory.findIndex(
|
||||||
|
(group) =>
|
||||||
|
group.label ===
|
||||||
|
st('templateWorkflows.category.ComfyUI Examples', 'ComfyUI Examples')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (comfyExamplesGroupIndex !== -1) {
|
||||||
|
groupedByCategory[comfyExamplesGroupIndex].modules.unshift(
|
||||||
|
createAllCategory()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedByCategory
|
||||||
})
|
})
|
||||||
|
|
||||||
async function loadWorkflowTemplates() {
|
async function loadWorkflowTemplates() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface TemplateInfo {
|
|||||||
description: string
|
description: string
|
||||||
localizedTitle?: string
|
localizedTitle?: string
|
||||||
localizedDescription?: string
|
localizedDescription?: string
|
||||||
|
sourceModule?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowTemplates {
|
export interface WorkflowTemplates {
|
||||||
|
|||||||
301
tests-ui/tests/composables/useTemplateWorkflows.test.ts
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import { flushPromises } from '@vue/test-utils'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
|
||||||
|
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
|
||||||
|
|
||||||
|
// Mock the store
|
||||||
|
vi.mock('@/stores/workflowTemplatesStore', () => ({
|
||||||
|
useWorkflowTemplatesStore: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the API
|
||||||
|
vi.mock('@/scripts/api', () => ({
|
||||||
|
api: {
|
||||||
|
fileURL: vi.fn((path) => `mock-file-url${path}`),
|
||||||
|
apiURL: vi.fn((path) => `mock-api-url${path}`)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the app
|
||||||
|
vi.mock('@/scripts/app', () => ({
|
||||||
|
app: {
|
||||||
|
loadGraphData: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock Vue I18n
|
||||||
|
vi.mock('vue-i18n', () => ({
|
||||||
|
useI18n: () => ({
|
||||||
|
t: vi.fn((key, fallback) => fallback || key)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock the dialog store
|
||||||
|
vi.mock('@/stores/dialogStore', () => ({
|
||||||
|
useDialogStore: vi.fn(() => ({
|
||||||
|
closeDialog: vi.fn()
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
global.fetch = vi.fn()
|
||||||
|
|
||||||
|
describe('useTemplateWorkflows', () => {
|
||||||
|
let mockWorkflowTemplatesStore: any
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockWorkflowTemplatesStore = {
|
||||||
|
isLoaded: false,
|
||||||
|
loadWorkflowTemplates: vi.fn().mockResolvedValue(true),
|
||||||
|
groupedTemplates: [
|
||||||
|
{
|
||||||
|
label: 'ComfyUI Examples',
|
||||||
|
modules: [
|
||||||
|
{
|
||||||
|
moduleName: 'all',
|
||||||
|
title: 'All',
|
||||||
|
localizedTitle: 'All Templates',
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
name: 'template1',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
sourceModule: 'default',
|
||||||
|
localizedTitle: 'Template 1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'template2',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
sourceModule: 'custom-module',
|
||||||
|
description: 'A custom template'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
moduleName: 'default',
|
||||||
|
title: 'Default',
|
||||||
|
localizedTitle: 'Default Templates',
|
||||||
|
templates: [
|
||||||
|
{
|
||||||
|
name: 'template1',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
localizedTitle: 'Template 1',
|
||||||
|
localizedDescription: 'A default template'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(useWorkflowTemplatesStore).mockReturnValue(
|
||||||
|
mockWorkflowTemplatesStore
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock fetch response
|
||||||
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
|
json: vi.fn().mockResolvedValue({ workflow: 'data' })
|
||||||
|
} as unknown as Response)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load templates from store', async () => {
|
||||||
|
const { loadTemplates, isTemplatesLoaded } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
expect(isTemplatesLoaded.value).toBe(false)
|
||||||
|
|
||||||
|
await loadTemplates()
|
||||||
|
|
||||||
|
expect(mockWorkflowTemplatesStore.loadWorkflowTemplates).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should select the first template category', () => {
|
||||||
|
const { selectFirstTemplateCategory, selectedTemplate } =
|
||||||
|
useTemplateWorkflows()
|
||||||
|
|
||||||
|
selectFirstTemplateCategory()
|
||||||
|
|
||||||
|
expect(selectedTemplate.value).toEqual(
|
||||||
|
mockWorkflowTemplatesStore.groupedTemplates[0].modules[0]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should select a template category', () => {
|
||||||
|
const { selectTemplateCategory, selectedTemplate } = useTemplateWorkflows()
|
||||||
|
const category = mockWorkflowTemplatesStore.groupedTemplates[0].modules[1] // Default category
|
||||||
|
|
||||||
|
const result = selectTemplateCategory(category)
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(selectedTemplate.value).toEqual(category)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format template thumbnails correctly for default templates', () => {
|
||||||
|
const { getTemplateThumbnailUrl } = useTemplateWorkflows()
|
||||||
|
const template = {
|
||||||
|
name: 'test-template',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
mediaType: 'image',
|
||||||
|
description: 'Test template'
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = getTemplateThumbnailUrl(template, 'default', '1')
|
||||||
|
|
||||||
|
expect(url).toBe('mock-file-url/templates/test-template-1.jpg')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format template thumbnails correctly for custom templates', () => {
|
||||||
|
const { getTemplateThumbnailUrl } = useTemplateWorkflows()
|
||||||
|
const template = {
|
||||||
|
name: 'test-template',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
mediaType: 'image',
|
||||||
|
description: 'Test template'
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = getTemplateThumbnailUrl(template, 'custom-module')
|
||||||
|
|
||||||
|
expect(url).toBe(
|
||||||
|
'mock-api-url/workflow_templates/custom-module/test-template.jpg'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format template titles correctly', () => {
|
||||||
|
const { getTemplateTitle } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
// Default template with localized title
|
||||||
|
const titleWithLocalized = getTemplateTitle(
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
localizedTitle: 'Localized Title',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
description: 'Test'
|
||||||
|
},
|
||||||
|
'default'
|
||||||
|
)
|
||||||
|
expect(titleWithLocalized).toBe('Localized Title')
|
||||||
|
|
||||||
|
// Default template without localized title
|
||||||
|
const titleWithFallback = getTemplateTitle(
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
title: 'Title',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
description: 'Test'
|
||||||
|
},
|
||||||
|
'default'
|
||||||
|
)
|
||||||
|
expect(titleWithFallback).toBe('Title')
|
||||||
|
|
||||||
|
// Custom template
|
||||||
|
const customTitle = getTemplateTitle(
|
||||||
|
{
|
||||||
|
name: 'test-template',
|
||||||
|
title: 'Custom Title',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
description: 'Test'
|
||||||
|
},
|
||||||
|
'custom-module'
|
||||||
|
)
|
||||||
|
expect(customTitle).toBe('Custom Title')
|
||||||
|
|
||||||
|
// Fallback to name
|
||||||
|
const nameOnly = getTemplateTitle(
|
||||||
|
{
|
||||||
|
name: 'name-only',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
description: 'Test'
|
||||||
|
},
|
||||||
|
'custom-module'
|
||||||
|
)
|
||||||
|
expect(nameOnly).toBe('name-only')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should format template descriptions correctly', () => {
|
||||||
|
const { getTemplateDescription } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
// Default template with localized description
|
||||||
|
const descWithLocalized = getTemplateDescription(
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
localizedDescription: 'Localized Description',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg',
|
||||||
|
description: 'Test'
|
||||||
|
},
|
||||||
|
'default'
|
||||||
|
)
|
||||||
|
expect(descWithLocalized).toBe('Localized Description')
|
||||||
|
|
||||||
|
// Custom template with description
|
||||||
|
const customDesc = getTemplateDescription(
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
description: 'custom-template_description',
|
||||||
|
mediaType: 'image',
|
||||||
|
mediaSubtype: 'jpg'
|
||||||
|
},
|
||||||
|
'custom-module'
|
||||||
|
)
|
||||||
|
expect(customDesc).toBe('custom template description')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load a template from the "All" category', async () => {
|
||||||
|
const { loadWorkflowTemplate, loadingTemplateId } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
// Set the store as loaded
|
||||||
|
mockWorkflowTemplatesStore.isLoaded = true
|
||||||
|
|
||||||
|
// Load a template from the "All" category
|
||||||
|
const result = await loadWorkflowTemplate('template1', 'all')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
|
||||||
|
expect(loadingTemplateId.value).toBe(null) // Should reset after loading
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load a template from a regular category', async () => {
|
||||||
|
const { loadWorkflowTemplate } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
// Set the store as loaded
|
||||||
|
mockWorkflowTemplatesStore.isLoaded = true
|
||||||
|
|
||||||
|
// Load a template from the default category
|
||||||
|
const result = await loadWorkflowTemplate('template1', 'default')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle errors when loading templates', async () => {
|
||||||
|
const { loadWorkflowTemplate, loadingTemplateId } = useTemplateWorkflows()
|
||||||
|
|
||||||
|
// Set the store as loaded
|
||||||
|
mockWorkflowTemplatesStore.isLoaded = true
|
||||||
|
|
||||||
|
// Mock fetch to throw an error
|
||||||
|
vi.mocked(fetch).mockRejectedValueOnce(new Error('Failed to fetch'))
|
||||||
|
|
||||||
|
// Spy on console.error
|
||||||
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
|
||||||
|
// Load a template that will fail
|
||||||
|
const result = await loadWorkflowTemplate('error-template', 'default')
|
||||||
|
|
||||||
|
expect(result).toBe(false)
|
||||||
|
expect(consoleSpy).toHaveBeenCalled()
|
||||||
|
expect(loadingTemplateId.value).toBe(null) // Should reset even after error
|
||||||
|
|
||||||
|
// Restore console.error
|
||||||
|
consoleSpy.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||