mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: open template via URL in linear mode (#6945)
## Summary Adds `mode` as a valid url query param when opening template from URL. https://github.com/user-attachments/assets/8e7efb1c-d842-4953-822a-f3cea7ddecb6 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6945-feat-open-template-via-URL-in-linear-mode-2b76d73d365081d8ad4af3d49a68a4ff) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
|
||||
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
import { useTemplateWorkflows } from './useTemplateWorkflows'
|
||||
|
||||
@@ -13,9 +14,10 @@ import { useTemplateWorkflows } from './useTemplateWorkflows'
|
||||
* Supports URLs like:
|
||||
* - /?template=flux_simple (loads with default source)
|
||||
* - /?template=flux_simple&source=custom (loads from custom source)
|
||||
* - /?template=flux_simple&mode=linear (loads template in linear mode)
|
||||
*
|
||||
* Input validation:
|
||||
* - Template and source parameters must match: ^[a-zA-Z0-9_-]+$
|
||||
* - Template, source, and mode parameters must match: ^[a-zA-Z0-9_-]+$
|
||||
* - Invalid formats are rejected with console warnings
|
||||
*/
|
||||
export function useTemplateUrlLoader() {
|
||||
@@ -24,7 +26,10 @@ export function useTemplateUrlLoader() {
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const templateWorkflows = useTemplateWorkflows()
|
||||
const canvasStore = useCanvasStore()
|
||||
const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
|
||||
const SUPPORTED_MODES = ['linear'] as const
|
||||
type SupportedMode = (typeof SUPPORTED_MODES)[number]
|
||||
|
||||
/**
|
||||
* Validates parameter format to prevent path traversal and injection attacks
|
||||
@@ -34,12 +39,20 @@ export function useTemplateUrlLoader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes template and source parameters from URL
|
||||
* Type guard to check if a value is a supported mode
|
||||
*/
|
||||
const isSupportedMode = (mode: string): mode is SupportedMode => {
|
||||
return SUPPORTED_MODES.includes(mode as SupportedMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes template, source, and mode parameters from URL
|
||||
*/
|
||||
const cleanupUrlParams = () => {
|
||||
const newQuery = { ...route.query }
|
||||
delete newQuery.template
|
||||
delete newQuery.source
|
||||
delete newQuery.mode
|
||||
void router.replace({ query: newQuery })
|
||||
}
|
||||
|
||||
@@ -70,6 +83,24 @@ export function useTemplateUrlLoader() {
|
||||
return
|
||||
}
|
||||
|
||||
const modeParam = route.query.mode as string | undefined
|
||||
|
||||
if (
|
||||
modeParam &&
|
||||
(typeof modeParam !== 'string' || !isValidParameter(modeParam))
|
||||
) {
|
||||
console.warn(
|
||||
`[useTemplateUrlLoader] Invalid mode parameter format: ${modeParam}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (modeParam && !isSupportedMode(modeParam)) {
|
||||
console.warn(
|
||||
`[useTemplateUrlLoader] Unsupported mode parameter: ${modeParam}. Supported modes: ${SUPPORTED_MODES.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await templateWorkflows.loadTemplates()
|
||||
|
||||
@@ -87,6 +118,9 @@ export function useTemplateUrlLoader() {
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
} else if (modeParam === 'linear') {
|
||||
// Set linear mode after successful template load
|
||||
canvasStore.linearMode = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
||||
@@ -80,7 +80,7 @@ const router = createRouter({
|
||||
installPreservedQueryTracker(router, [
|
||||
{
|
||||
namespace: PRESERVED_QUERY_NAMESPACES.TEMPLATE,
|
||||
keys: ['template', 'source']
|
||||
keys: ['template', 'source', 'mode']
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ import { useTemplateUrlLoader } from '@/platform/workflow/templates/composables/
|
||||
* Tests the behavior of loading templates via URL query parameters:
|
||||
* - ?template=flux_simple loads the template
|
||||
* - ?template=flux_simple&source=custom loads from custom source
|
||||
* - ?template=flux_simple&mode=linear loads template in linear mode
|
||||
* - Invalid template shows error toast
|
||||
* - Input validation for template and source parameters
|
||||
* - Input validation for template, source, and mode parameters
|
||||
*/
|
||||
|
||||
const preservedQueryMocks = vi.hoisted(() => ({
|
||||
@@ -70,10 +71,20 @@ vi.mock('vue-i18n', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock canvas store
|
||||
const mockCanvasStore = {
|
||||
linearMode: false
|
||||
}
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => mockCanvasStore
|
||||
}))
|
||||
|
||||
describe('useTemplateUrlLoader', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockQueryParams = {}
|
||||
mockCanvasStore.linearMode = false
|
||||
})
|
||||
|
||||
it('does not load template when no query param present', () => {
|
||||
@@ -236,6 +247,7 @@ describe('useTemplateUrlLoader', () => {
|
||||
mockQueryParams = {
|
||||
template: 'flux_simple',
|
||||
source: 'custom',
|
||||
mode: 'linear',
|
||||
other: 'param'
|
||||
}
|
||||
|
||||
@@ -270,4 +282,121 @@ describe('useTemplateUrlLoader', () => {
|
||||
query: { other: 'param' }
|
||||
})
|
||||
})
|
||||
|
||||
it('sets linear mode when mode=linear and template loads successfully', async () => {
|
||||
mockQueryParams = { template: 'flux_simple', mode: 'linear' }
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
await loadTemplateFromUrl()
|
||||
|
||||
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
|
||||
'flux_simple',
|
||||
'default'
|
||||
)
|
||||
expect(mockCanvasStore.linearMode).toBe(true)
|
||||
})
|
||||
|
||||
it('does not set linear mode when template loading fails', async () => {
|
||||
mockQueryParams = { template: 'invalid-template', mode: 'linear' }
|
||||
mockLoadWorkflowTemplate.mockResolvedValueOnce(false)
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
await loadTemplateFromUrl()
|
||||
|
||||
expect(mockCanvasStore.linearMode).toBe(false)
|
||||
})
|
||||
|
||||
it('does not set linear mode when mode parameter is not linear', async () => {
|
||||
mockQueryParams = { template: 'flux_simple', mode: 'graph' }
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
await loadTemplateFromUrl()
|
||||
|
||||
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
|
||||
'flux_simple',
|
||||
'default'
|
||||
)
|
||||
expect(mockCanvasStore.linearMode).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects invalid mode parameter with special characters', () => {
|
||||
mockQueryParams = { template: 'flux_simple', mode: '../malicious' }
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
void loadTemplateFromUrl()
|
||||
|
||||
expect(mockLoadTemplates).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles array mode params correctly', () => {
|
||||
// Vue Router can return string[] for duplicate params
|
||||
mockQueryParams = {
|
||||
template: 'flux_simple',
|
||||
mode: ['linear', 'graph'] as any
|
||||
}
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
void loadTemplateFromUrl()
|
||||
|
||||
// Should not load when mode param is an array
|
||||
expect(mockLoadTemplates).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('warns about unsupported mode values but continues loading', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
mockQueryParams = { template: 'flux_simple', mode: 'unsupported' }
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
await loadTemplateFromUrl()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'[useTemplateUrlLoader] Unsupported mode parameter: unsupported. Supported modes: linear'
|
||||
)
|
||||
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
|
||||
'flux_simple',
|
||||
'default'
|
||||
)
|
||||
expect(mockCanvasStore.linearMode).toBe(false)
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('accepts supported mode parameter: linear', async () => {
|
||||
mockQueryParams = { template: 'flux_simple', mode: 'linear' }
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
await loadTemplateFromUrl()
|
||||
|
||||
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
|
||||
'flux_simple',
|
||||
'default'
|
||||
)
|
||||
expect(mockCanvasStore.linearMode).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts valid format but warns about unsupported modes', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const unsupportedModes = ['graph', 'mode123', 'my_mode-2']
|
||||
|
||||
for (const mode of unsupportedModes) {
|
||||
vi.clearAllMocks()
|
||||
consoleSpy.mockClear()
|
||||
mockCanvasStore.linearMode = false
|
||||
mockQueryParams = { template: 'flux_simple', mode }
|
||||
|
||||
const { loadTemplateFromUrl } = useTemplateUrlLoader()
|
||||
await loadTemplateFromUrl()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
`[useTemplateUrlLoader] Unsupported mode parameter: ${mode}. Supported modes: linear`
|
||||
)
|
||||
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
|
||||
'flux_simple',
|
||||
'default'
|
||||
)
|
||||
expect(mockCanvasStore.linearMode).toBe(false)
|
||||
}
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user