mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
## Summary - Track entering app mode from template URL (`source: template_url`) and default view dialog (`source: default_view_dialog`) - Tag shared workflow loads with `openSource: 'shared'` instead of defaulting to `'unknown'` - Rename telemetry event from `app:toggle_linear_mode` to `app:app_mode_opened` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9720-feat-track-app-mode-entry-and-shared-workflow-loading-31f6d73d365081af8c6ae3247a50cf3f) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
4.3 KiB
TypeScript
148 lines
4.3 KiB
TypeScript
import { useToast } from 'primevue/usetoast'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
|
|
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
|
|
import { useTemplateWorkflows } from './useTemplateWorkflows'
|
|
|
|
/**
|
|
* Composable for loading templates from URL query parameters
|
|
*
|
|
* 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, source, and mode parameters must match: ^[a-zA-Z0-9_-]+$
|
|
* - Invalid formats are rejected with console warnings
|
|
*/
|
|
export function useTemplateUrlLoader() {
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
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
|
|
* Allows: letters, numbers, underscores, hyphens, and dots (for version numbers)
|
|
* Blocks: path separators (/, \), special chars that could enable injection
|
|
*/
|
|
const isValidParameter = (param: string): boolean => {
|
|
return /^[a-zA-Z0-9_.-]+$/.test(param)
|
|
}
|
|
|
|
/**
|
|
* 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 })
|
|
}
|
|
|
|
/**
|
|
* Loads template from URL query parameters if present
|
|
* Handles errors internally and shows appropriate user feedback
|
|
*/
|
|
const loadTemplateFromUrl = async () => {
|
|
const templateParam = route.query.template
|
|
|
|
if (!templateParam || typeof templateParam !== 'string') {
|
|
return
|
|
}
|
|
|
|
if (!isValidParameter(templateParam)) {
|
|
console.warn(
|
|
`[useTemplateUrlLoader] Invalid template parameter format: ${templateParam}`
|
|
)
|
|
return
|
|
}
|
|
|
|
const sourceParam = (route.query.source as string | undefined) || 'default'
|
|
|
|
if (!isValidParameter(sourceParam)) {
|
|
console.warn(
|
|
`[useTemplateUrlLoader] Invalid source parameter format: ${sourceParam}`
|
|
)
|
|
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()
|
|
|
|
const success = await templateWorkflows.loadWorkflowTemplate(
|
|
templateParam,
|
|
sourceParam
|
|
)
|
|
|
|
if (!success) {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: t('g.error'),
|
|
detail: t('templateWorkflows.error.templateNotFound', {
|
|
templateName: templateParam
|
|
})
|
|
})
|
|
} else if (modeParam === 'linear') {
|
|
// Set linear mode after successful template load
|
|
useTelemetry()?.trackEnterLinear({ source: 'template_url' })
|
|
canvasStore.linearMode = true
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
'[useTemplateUrlLoader] Failed to load template from URL:',
|
|
error
|
|
)
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: t('g.error'),
|
|
detail: t('g.errorLoadingTemplate')
|
|
})
|
|
} finally {
|
|
cleanupUrlParams()
|
|
clearPreservedQuery(TEMPLATE_NAMESPACE)
|
|
}
|
|
}
|
|
|
|
return {
|
|
loadTemplateFromUrl
|
|
}
|
|
}
|