Skip to content
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 60 additions & 1 deletion src/components/templates/TemplateWorkflowCard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,69 @@ vi.mock('@vueuse/core', () => ({
vi.mock('@/scripts/api', () => ({
api: {
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(),
fetchTemplateJson: vi.fn()
})
}))

describe('TemplateWorkflowCard', () => {
const createTemplate = (overrides = {}): TemplateInfo => ({
name: 'test-template',
Expand Down
48 changes: 24 additions & 24 deletions src/components/templates/TemplateWorkflowCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import { api } from '@/scripts/api'
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
import { TemplateInfo } from '@/types/workflowTemplateTypes'

const UPSCALE_ZOOM_SCALE = 16 // for upscale templates, exaggerate the hover zoom
Expand All @@ -102,36 +102,36 @@ const { sourceModule, loading, template } = defineProps<{
const cardRef = ref<HTMLElement | null>(null)
const isHovered = useElementHover(cardRef)

const getThumbnailUrl = (index = '') => {
const basePath =
sourceModule === 'default'
? api.fileURL(`/templates/${template.name}`)
: api.apiURL(`/workflow_templates/${sourceModule}/${template.name}`)
const { getTemplateThumbnailUrl, getTemplateTitle, getTemplateDescription } =
useTemplateWorkflows()

// For templates from custom nodes, multiple images is not yet supported
const indexSuffix = sourceModule === 'default' && index ? `-${index}` : ''

return `${basePath}${indexSuffix}.${template.mediaSubtype}`
}
// Determine the effective source module to use (from template or prop)
const effectiveSourceModule = computed(
() => template.sourceModule || sourceModule
)

const baseThumbnailSrc = computed(() =>
getThumbnailUrl(sourceModule === 'default' ? '1' : '')
getTemplateThumbnailUrl(
template,
effectiveSourceModule.value,
effectiveSourceModule.value === 'default' ? '1' : ''
)
)

const overlayThumbnailSrc = computed(() =>
getThumbnailUrl(sourceModule === 'default' ? '2' : '')
getTemplateThumbnailUrl(
template,
effectiveSourceModule.value,
effectiveSourceModule.value === 'default' ? '2' : ''
)
)

const description = computed(() => {
return sourceModule === 'default'
? template.localizedDescription ?? ''
: template.description.replace(/[-_]/g, ' ').trim()
})

const title = computed(() => {
return sourceModule === 'default'
? template.localizedTitle ?? ''
: template.name
})
const description = computed(() =>
getTemplateDescription(template, effectiveSourceModule.value)
)
const title = computed(() =>
getTemplateTitle(template, effectiveSourceModule.value)
)

defineEmits<{
loadWorkflow: [name: string]
Expand Down
48 changes: 30 additions & 18 deletions src/components/templates/TemplateWorkflowList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,38 @@
>
<Column field="title" :header="$t('g.title')">
<template #body="slotProps">
<span :title="getTemplateTitle(slotProps.data)">{{
getTemplateTitle(slotProps.data)
}}</span>
<span
:title="
getTemplateTitle(
slotProps.data,
slotProps.data.sourceModule || sourceModule
)
"
>{{
getTemplateTitle(
slotProps.data,
slotProps.data.sourceModule || sourceModule
)
}}</span
>
</template>
</Column>
<Column field="description" :header="$t('g.description')">
<template #body="slotProps">
<span :title="getTemplateDescription(slotProps.data)">
{{ getTemplateDescription(slotProps.data) }}
<span
:title="
getTemplateDescription(
slotProps.data,
slotProps.data.sourceModule || sourceModule
)
"
>
{{
getTemplateDescription(
slotProps.data,
slotProps.data.sourceModule || sourceModule
)
}}
</span>
</template>
</Column>
Expand All @@ -40,6 +63,7 @@ import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import { ref } from 'vue'

import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
import type { TemplateInfo } from '@/types/workflowTemplateTypes'

const { sourceModule, loading, templates } = defineProps<{
Expand All @@ -50,21 +74,9 @@ const { sourceModule, loading, templates } = defineProps<{
}>()

const selectedTemplate = ref(null)
const { getTemplateTitle, getTemplateDescription } = useTemplateWorkflows()

const emit = defineEmits<{
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>
96 changes: 37 additions & 59 deletions src/components/templates/TemplateWorkflowsContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<ProgressSpinner
v-if="!workflowTemplatesStore.isLoaded || !isReady"
v-if="!isTemplatesLoaded || !isReady"
class="absolute w-8 h-full inset-0"
/>
<TemplateWorkflowsSideNav
:tabs="tabs"
:selected-tab="selectedTab"
:tabs="allTemplateGroups"
:selected-tab="selectedTemplate"
@update:selected-tab="handleTabSelection"
/>
</aside>
Expand All @@ -37,14 +37,14 @@
}"
>
<TemplateWorkflowView
v-if="isReady && selectedTab"
v-if="isReady && selectedTemplate"
class="px-12 py-4"
:title="selectedTab.title"
:source-module="selectedTab.moduleName"
:templates="selectedTab.templates"
:loading="workflowLoading"
:category-title="selectedTab.title"
@load-workflow="loadWorkflow"
:title="selectedTemplate.title"
:source-module="selectedTemplate.moduleName"
:templates="selectedTemplate.templates"
:loading="loadingTemplateId"
:category-title="selectedTemplate.title"
@load-workflow="handleLoadWorkflow"
/>
</div>
</div>
Expand All @@ -56,47 +56,46 @@ import { useAsyncState } from '@vueuse/core'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { watch } from 'vue'

import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
import TemplateWorkflowsSideNav from '@/components/templates/TemplateWorkflowsSideNav.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useDialogStore } from '@/stores/dialogStore'
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
import type { WorkflowTemplates } from '@/types/workflowTemplateTypes'

const { t } = useI18n()

const {
isSmallScreen,
isOpen: isSideNavOpen,
toggle: toggleSideNav
} = useResponsiveCollapse()

const workflowTemplatesStore = useWorkflowTemplatesStore()
const { isReady } = useAsyncState(
workflowTemplatesStore.loadWorkflowTemplates,
null
)

const selectedTab = ref<WorkflowTemplates | null>(null)
const selectFirstTab = () => {
const firstTab = workflowTemplatesStore.groupedTemplates[0].modules[0]
handleTabSelection(firstTab)
}
watch(isReady, selectFirstTab, { once: true })
const {
selectedTemplate,
loadingTemplateId,
isTemplatesLoaded,
allTemplateGroups,
loadTemplates,
selectFirstTemplateCategory,
selectTemplateCategory,
loadWorkflowTemplate
} = useTemplateWorkflows()

const workflowLoading = ref<string | null>(null)
const { isReady } = useAsyncState(loadTemplates, null)

const tabs = computed(() => workflowTemplatesStore.groupedTemplates)
watch(
isReady,
() => {
if (isReady.value) {
selectFirstTemplateCategory()
}
},
{ once: true }
)

const handleTabSelection = (selection: WorkflowTemplates | null) => {
//Listbox allows deselecting so this special case is ignored here
if (selection !== selectedTab.value && selection !== null) {
selectedTab.value = selection
if (selection !== null) {
selectTemplateCategory(selection)

// On small screens, close the sidebar when a category is selected
if (isSmallScreen.value) {
Expand All @@ -105,30 +104,9 @@ const handleTabSelection = (selection: WorkflowTemplates | null) => {
}
}

const loadWorkflow = async (id: string) => {
if (!isReady.value) return

workflowLoading.value = id
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)
const handleLoadWorkflow = async (id: string) => {
if (!isReady.value || !selectedTemplate.value) return false

return false
return loadWorkflowTemplate(id, selectedTemplate.value.moduleName)
}
</script>
1 change: 1 addition & 0 deletions src/composables/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Composables for sidebar functionality:
- `useNodeLibrarySidebarTab` - Manages the node library sidebar tab
- `useQueueSidebarTab` - Manages the queue sidebar tab
- `useWorkflowsSidebarTab` - Manages the workflows sidebar tab
- `useTemplateWorkflows` - Manages template workflow loading, selection, and display

### Widgets

Expand Down
Loading