Skip to content

Commit c5fe617

Browse files
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)
1 parent 8b2c1fc commit c5fe617

File tree

3 files changed

+167
-4
lines changed

3 files changed

+167
-4
lines changed

src/platform/workflow/templates/composables/useTemplateUrlLoader.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
44

55
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
66
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
7+
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
78

89
import { useTemplateWorkflows } from './useTemplateWorkflows'
910

@@ -13,9 +14,10 @@ import { useTemplateWorkflows } from './useTemplateWorkflows'
1314
* Supports URLs like:
1415
* - /?template=flux_simple (loads with default source)
1516
* - /?template=flux_simple&source=custom (loads from custom source)
17+
* - /?template=flux_simple&mode=linear (loads template in linear mode)
1618
*
1719
* Input validation:
18-
* - Template and source parameters must match: ^[a-zA-Z0-9_-]+$
20+
* - Template, source, and mode parameters must match: ^[a-zA-Z0-9_-]+$
1921
* - Invalid formats are rejected with console warnings
2022
*/
2123
export function useTemplateUrlLoader() {
@@ -24,7 +26,10 @@ export function useTemplateUrlLoader() {
2426
const { t } = useI18n()
2527
const toast = useToast()
2628
const templateWorkflows = useTemplateWorkflows()
29+
const canvasStore = useCanvasStore()
2730
const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
31+
const SUPPORTED_MODES = ['linear'] as const
32+
type SupportedMode = (typeof SUPPORTED_MODES)[number]
2833

2934
/**
3035
* Validates parameter format to prevent path traversal and injection attacks
@@ -34,12 +39,20 @@ export function useTemplateUrlLoader() {
3439
}
3540

3641
/**
37-
* Removes template and source parameters from URL
42+
* Type guard to check if a value is a supported mode
43+
*/
44+
const isSupportedMode = (mode: string): mode is SupportedMode => {
45+
return SUPPORTED_MODES.includes(mode as SupportedMode)
46+
}
47+
48+
/**
49+
* Removes template, source, and mode parameters from URL
3850
*/
3951
const cleanupUrlParams = () => {
4052
const newQuery = { ...route.query }
4153
delete newQuery.template
4254
delete newQuery.source
55+
delete newQuery.mode
4356
void router.replace({ query: newQuery })
4457
}
4558

@@ -70,6 +83,24 @@ export function useTemplateUrlLoader() {
7083
return
7184
}
7285

86+
const modeParam = route.query.mode as string | undefined
87+
88+
if (
89+
modeParam &&
90+
(typeof modeParam !== 'string' || !isValidParameter(modeParam))
91+
) {
92+
console.warn(
93+
`[useTemplateUrlLoader] Invalid mode parameter format: ${modeParam}`
94+
)
95+
return
96+
}
97+
98+
if (modeParam && !isSupportedMode(modeParam)) {
99+
console.warn(
100+
`[useTemplateUrlLoader] Unsupported mode parameter: ${modeParam}. Supported modes: ${SUPPORTED_MODES.join(', ')}`
101+
)
102+
}
103+
73104
try {
74105
await templateWorkflows.loadTemplates()
75106

@@ -87,6 +118,9 @@ export function useTemplateUrlLoader() {
87118
}),
88119
life: 3000
89120
})
121+
} else if (modeParam === 'linear') {
122+
// Set linear mode after successful template load
123+
canvasStore.linearMode = true
90124
}
91125
} catch (error) {
92126
console.error(

src/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const router = createRouter({
8080
installPreservedQueryTracker(router, [
8181
{
8282
namespace: PRESERVED_QUERY_NAMESPACES.TEMPLATE,
83-
keys: ['template', 'source']
83+
keys: ['template', 'source', 'mode']
8484
}
8585
])
8686

tests-ui/tests/platform/workflow/templates/composables/useTemplateUrlLoader.test.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { useTemplateUrlLoader } from '@/platform/workflow/templates/composables/
88
* Tests the behavior of loading templates via URL query parameters:
99
* - ?template=flux_simple loads the template
1010
* - ?template=flux_simple&source=custom loads from custom source
11+
* - ?template=flux_simple&mode=linear loads template in linear mode
1112
* - Invalid template shows error toast
12-
* - Input validation for template and source parameters
13+
* - Input validation for template, source, and mode parameters
1314
*/
1415

1516
const preservedQueryMocks = vi.hoisted(() => ({
@@ -70,10 +71,20 @@ vi.mock('vue-i18n', () => ({
7071
})
7172
}))
7273

74+
// Mock canvas store
75+
const mockCanvasStore = {
76+
linearMode: false
77+
}
78+
79+
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
80+
useCanvasStore: () => mockCanvasStore
81+
}))
82+
7383
describe('useTemplateUrlLoader', () => {
7484
beforeEach(() => {
7585
vi.clearAllMocks()
7686
mockQueryParams = {}
87+
mockCanvasStore.linearMode = false
7788
})
7889

7990
it('does not load template when no query param present', () => {
@@ -236,6 +247,7 @@ describe('useTemplateUrlLoader', () => {
236247
mockQueryParams = {
237248
template: 'flux_simple',
238249
source: 'custom',
250+
mode: 'linear',
239251
other: 'param'
240252
}
241253

@@ -270,4 +282,121 @@ describe('useTemplateUrlLoader', () => {
270282
query: { other: 'param' }
271283
})
272284
})
285+
286+
it('sets linear mode when mode=linear and template loads successfully', async () => {
287+
mockQueryParams = { template: 'flux_simple', mode: 'linear' }
288+
289+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
290+
await loadTemplateFromUrl()
291+
292+
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
293+
'flux_simple',
294+
'default'
295+
)
296+
expect(mockCanvasStore.linearMode).toBe(true)
297+
})
298+
299+
it('does not set linear mode when template loading fails', async () => {
300+
mockQueryParams = { template: 'invalid-template', mode: 'linear' }
301+
mockLoadWorkflowTemplate.mockResolvedValueOnce(false)
302+
303+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
304+
await loadTemplateFromUrl()
305+
306+
expect(mockCanvasStore.linearMode).toBe(false)
307+
})
308+
309+
it('does not set linear mode when mode parameter is not linear', async () => {
310+
mockQueryParams = { template: 'flux_simple', mode: 'graph' }
311+
312+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
313+
await loadTemplateFromUrl()
314+
315+
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
316+
'flux_simple',
317+
'default'
318+
)
319+
expect(mockCanvasStore.linearMode).toBe(false)
320+
})
321+
322+
it('rejects invalid mode parameter with special characters', () => {
323+
mockQueryParams = { template: 'flux_simple', mode: '../malicious' }
324+
325+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
326+
void loadTemplateFromUrl()
327+
328+
expect(mockLoadTemplates).not.toHaveBeenCalled()
329+
})
330+
331+
it('handles array mode params correctly', () => {
332+
// Vue Router can return string[] for duplicate params
333+
mockQueryParams = {
334+
template: 'flux_simple',
335+
mode: ['linear', 'graph'] as any
336+
}
337+
338+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
339+
void loadTemplateFromUrl()
340+
341+
// Should not load when mode param is an array
342+
expect(mockLoadTemplates).not.toHaveBeenCalled()
343+
})
344+
345+
it('warns about unsupported mode values but continues loading', async () => {
346+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
347+
mockQueryParams = { template: 'flux_simple', mode: 'unsupported' }
348+
349+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
350+
await loadTemplateFromUrl()
351+
352+
expect(consoleSpy).toHaveBeenCalledWith(
353+
'[useTemplateUrlLoader] Unsupported mode parameter: unsupported. Supported modes: linear'
354+
)
355+
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
356+
'flux_simple',
357+
'default'
358+
)
359+
expect(mockCanvasStore.linearMode).toBe(false)
360+
361+
consoleSpy.mockRestore()
362+
})
363+
364+
it('accepts supported mode parameter: linear', async () => {
365+
mockQueryParams = { template: 'flux_simple', mode: 'linear' }
366+
367+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
368+
await loadTemplateFromUrl()
369+
370+
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
371+
'flux_simple',
372+
'default'
373+
)
374+
expect(mockCanvasStore.linearMode).toBe(true)
375+
})
376+
377+
it('accepts valid format but warns about unsupported modes', async () => {
378+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
379+
const unsupportedModes = ['graph', 'mode123', 'my_mode-2']
380+
381+
for (const mode of unsupportedModes) {
382+
vi.clearAllMocks()
383+
consoleSpy.mockClear()
384+
mockCanvasStore.linearMode = false
385+
mockQueryParams = { template: 'flux_simple', mode }
386+
387+
const { loadTemplateFromUrl } = useTemplateUrlLoader()
388+
await loadTemplateFromUrl()
389+
390+
expect(consoleSpy).toHaveBeenCalledWith(
391+
`[useTemplateUrlLoader] Unsupported mode parameter: ${mode}. Supported modes: linear`
392+
)
393+
expect(mockLoadWorkflowTemplate).toHaveBeenCalledWith(
394+
'flux_simple',
395+
'default'
396+
)
397+
expect(mockCanvasStore.linearMode).toBe(false)
398+
}
399+
400+
consoleSpy.mockRestore()
401+
})
273402
})

0 commit comments

Comments
 (0)