Skip to content

Commit 8b8f353

Browse files
fix: template query param stripped during login views (#6677)
Fixes issue where query params from #6593 are stripped during the login/signup views/flow by storing initial params in session storage via router plugin. https://github.com/user-attachments/assets/51642e8c-af5c-43ef-ab7d-133bc7e511aa ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6677-fix-template-query-param-stripped-during-login-views-2aa6d73d365081a1bdc7d22b35f72a77) by [Unito](https://www.unito.io)
1 parent ecd87ae commit 8b8f353

File tree

8 files changed

+262
-3
lines changed

8 files changed

+262
-3
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { LocationQuery, LocationQueryRaw } from 'vue-router'
2+
3+
const STORAGE_PREFIX = 'Comfy.PreservedQuery.'
4+
const preservedQueries = new Map<string, Record<string, string>>()
5+
6+
const readQueryParam = (value: unknown): string | undefined => {
7+
return typeof value === 'string' ? value : undefined
8+
}
9+
10+
const getStorageKey = (namespace: string) => `${STORAGE_PREFIX}${namespace}`
11+
12+
const isValidQueryRecord = (
13+
value: unknown
14+
): value is Record<string, string> => {
15+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
16+
return false
17+
}
18+
return Object.values(value).every((v) => typeof v === 'string')
19+
}
20+
21+
const readFromStorage = (namespace: string): Record<string, string> | null => {
22+
try {
23+
const raw = sessionStorage.getItem(getStorageKey(namespace))
24+
if (!raw) return null
25+
26+
const parsed = JSON.parse(raw)
27+
if (!isValidQueryRecord(parsed)) {
28+
console.warn('[preservedQuery] invalid storage format')
29+
sessionStorage.removeItem(getStorageKey(namespace))
30+
return null
31+
}
32+
return parsed
33+
} catch (error) {
34+
console.warn('[preservedQuery] storage operation failed')
35+
sessionStorage.removeItem(getStorageKey(namespace))
36+
return null
37+
}
38+
}
39+
40+
const writeToStorage = (
41+
namespace: string,
42+
payload: Record<string, string> | null
43+
) => {
44+
try {
45+
if (!payload || Object.keys(payload).length === 0) {
46+
sessionStorage.removeItem(getStorageKey(namespace))
47+
return
48+
}
49+
sessionStorage.setItem(getStorageKey(namespace), JSON.stringify(payload))
50+
} catch (error) {
51+
console.warn('[preservedQuery] failed to write storage', {
52+
namespace,
53+
error
54+
})
55+
}
56+
}
57+
58+
export const hydratePreservedQuery = (namespace: string) => {
59+
if (preservedQueries.has(namespace)) return
60+
const payload = readFromStorage(namespace)
61+
if (payload) {
62+
preservedQueries.set(namespace, payload)
63+
}
64+
}
65+
66+
export const capturePreservedQuery = (
67+
namespace: string,
68+
query: LocationQuery,
69+
keys: string[]
70+
) => {
71+
const payload: Record<string, string> = {}
72+
73+
keys.forEach((key) => {
74+
const value = readQueryParam(query[key])
75+
if (value) {
76+
payload[key] = value
77+
}
78+
})
79+
80+
if (Object.keys(payload).length === 0) return
81+
82+
preservedQueries.set(namespace, payload)
83+
writeToStorage(namespace, payload)
84+
}
85+
86+
export const mergePreservedQueryIntoQuery = (
87+
namespace: string,
88+
query?: LocationQueryRaw
89+
): LocationQueryRaw | undefined => {
90+
const payload = preservedQueries.get(namespace)
91+
if (!payload) return undefined
92+
93+
const nextQuery: LocationQueryRaw = { ...(query || {}) }
94+
let changed = false
95+
96+
for (const [key, value] of Object.entries(payload)) {
97+
if (typeof nextQuery[key] === 'string') continue
98+
nextQuery[key] = value
99+
changed = true
100+
}
101+
102+
return changed ? nextQuery : undefined
103+
}
104+
105+
export const clearPreservedQuery = (namespace: string) => {
106+
if (!preservedQueries.has(namespace)) return
107+
preservedQueries.delete(namespace)
108+
writeToStorage(namespace, null)
109+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const PRESERVED_QUERY_NAMESPACES = {
2+
TEMPLATE: 'template'
3+
} as const
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Router } from 'vue-router'
2+
3+
import {
4+
capturePreservedQuery,
5+
hydratePreservedQuery
6+
} from '@/platform/navigation/preservedQueryManager'
7+
8+
export const installPreservedQueryTracker = (
9+
router: Router,
10+
definitions: Array<{ namespace: string; keys: string[] }>
11+
) => {
12+
const trackedDefinitions = definitions.map((definition) => ({
13+
...definition
14+
}))
15+
16+
router.beforeEach((to, _from, next) => {
17+
const queryKeys = new Set(Object.keys(to.query))
18+
19+
trackedDefinitions.forEach(({ namespace, keys }) => {
20+
hydratePreservedQuery(namespace)
21+
const shouldCapture = keys.some((key) => queryKeys.has(key))
22+
if (shouldCapture) {
23+
capturePreservedQuery(namespace, to.query, keys)
24+
}
25+
})
26+
27+
next()
28+
})
29+
}

src/platform/workflow/persistence/composables/useWorkflowPersistence.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { tryOnScopeDispose } from '@vueuse/core'
22
import { computed, watch } from 'vue'
3-
import { useRoute } from 'vue-router'
3+
import { useRoute, useRouter } from 'vue-router'
44

5+
import {
6+
hydratePreservedQuery,
7+
mergePreservedQueryIntoQuery
8+
} from '@/platform/navigation/preservedQueryManager'
9+
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
510
import { useSettingStore } from '@/platform/settings/settingStore'
611
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
712
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -15,7 +20,23 @@ export function useWorkflowPersistence() {
1520
const workflowStore = useWorkflowStore()
1621
const settingStore = useSettingStore()
1722
const route = useRoute()
23+
const router = useRouter()
1824
const templateUrlLoader = useTemplateUrlLoader()
25+
const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
26+
27+
const ensureTemplateQueryFromIntent = async () => {
28+
hydratePreservedQuery(TEMPLATE_NAMESPACE)
29+
const mergedQuery = mergePreservedQueryIntoQuery(
30+
TEMPLATE_NAMESPACE,
31+
route.query
32+
)
33+
34+
if (mergedQuery) {
35+
await router.replace({ query: mergedQuery })
36+
}
37+
38+
return mergedQuery ?? route.query
39+
}
1940

2041
const workflowPersistenceEnabled = computed(() =>
2142
settingStore.get('Comfy.Workflow.Persist')
@@ -101,8 +122,8 @@ export function useWorkflowPersistence() {
101122
}
102123

103124
const loadTemplateFromUrlIfPresent = async () => {
104-
const hasTemplateUrl =
105-
route.query.template && typeof route.query.template === 'string'
125+
const query = await ensureTemplateQueryFromIntent()
126+
const hasTemplateUrl = query.template && typeof query.template === 'string'
106127

107128
if (hasTemplateUrl) {
108129
await templateUrlLoader.loadTemplateFromUrl()

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { useToast } from 'primevue/usetoast'
22
import { useI18n } from 'vue-i18n'
33
import { useRoute, useRouter } from 'vue-router'
44

5+
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
6+
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
7+
58
import { useTemplateWorkflows } from './useTemplateWorkflows'
69

710
/**
@@ -21,6 +24,7 @@ export function useTemplateUrlLoader() {
2124
const { t } = useI18n()
2225
const toast = useToast()
2326
const templateWorkflows = useTemplateWorkflows()
27+
const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
2428

2529
/**
2630
* Validates parameter format to prevent path traversal and injection attacks
@@ -97,6 +101,7 @@ export function useTemplateUrlLoader() {
97101
})
98102
} finally {
99103
cleanupUrlParams()
104+
clearPreservedQuery(TEMPLATE_NAMESPACE)
100105
}
101106
}
102107

src/router.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { useUserStore } from '@/stores/userStore'
1414
import { isElectron } from '@/utils/envUtil'
1515
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
1616

17+
import { installPreservedQueryTracker } from '@/platform/navigation/preservedQueryTracker'
18+
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
1719
import { cloudOnboardingRoutes } from './platform/cloud/onboarding/onboardingCloudRoutes'
1820

1921
const isFileProtocol = window.location.protocol === 'file:'
@@ -75,6 +77,13 @@ const router = createRouter({
7577
}
7678
})
7779

80+
installPreservedQueryTracker(router, [
81+
{
82+
namespace: PRESERVED_QUERY_NAMESPACES.TEMPLATE,
83+
keys: ['template', 'source']
84+
}
85+
])
86+
7887
if (isCloud) {
7988
const PUBLIC_ROUTE_NAMES = new Set([
8089
'cloud-login',
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { beforeEach, describe, expect, it } from 'vitest'
2+
3+
import {
4+
capturePreservedQuery,
5+
clearPreservedQuery,
6+
hydratePreservedQuery,
7+
mergePreservedQueryIntoQuery
8+
} from '@/platform/navigation/preservedQueryManager'
9+
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
10+
11+
const NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE
12+
13+
describe('preservedQueryManager', () => {
14+
beforeEach(() => {
15+
sessionStorage.clear()
16+
clearPreservedQuery(NAMESPACE)
17+
})
18+
19+
it('captures specified keys from the route query', () => {
20+
capturePreservedQuery(NAMESPACE, { template: 'flux', source: 'custom' }, [
21+
'template',
22+
'source'
23+
])
24+
25+
hydratePreservedQuery(NAMESPACE)
26+
const merged = mergePreservedQueryIntoQuery(NAMESPACE)
27+
28+
expect(merged).toEqual({ template: 'flux', source: 'custom' })
29+
expect(sessionStorage.getItem('Comfy.PreservedQuery.template')).toBeTruthy()
30+
})
31+
32+
it('hydrates cached payload from sessionStorage once', () => {
33+
sessionStorage.setItem(
34+
'Comfy.PreservedQuery.template',
35+
JSON.stringify({ template: 'flux', source: 'default' })
36+
)
37+
38+
hydratePreservedQuery(NAMESPACE)
39+
const merged = mergePreservedQueryIntoQuery(NAMESPACE)
40+
41+
expect(merged).toEqual({ template: 'flux', source: 'default' })
42+
})
43+
44+
it('merges stored payload only when query lacks the keys', () => {
45+
capturePreservedQuery(NAMESPACE, { template: 'flux' }, ['template'])
46+
47+
const merged = mergePreservedQueryIntoQuery(NAMESPACE, {
48+
foo: 'bar'
49+
})
50+
51+
expect(merged).toEqual({ foo: 'bar', template: 'flux' })
52+
})
53+
54+
it('returns undefined when merge does not change query', () => {
55+
capturePreservedQuery(NAMESPACE, { template: 'flux' }, ['template'])
56+
57+
const merged = mergePreservedQueryIntoQuery(NAMESPACE, {
58+
template: 'existing'
59+
})
60+
61+
expect(merged).toBeUndefined()
62+
})
63+
64+
it('clears cached payload', () => {
65+
capturePreservedQuery(NAMESPACE, { template: 'flux' }, ['template'])
66+
67+
clearPreservedQuery(NAMESPACE)
68+
69+
const merged = mergePreservedQueryIntoQuery(NAMESPACE)
70+
expect(merged).toBeUndefined()
71+
expect(sessionStorage.getItem('Comfy.PreservedQuery.template')).toBeNull()
72+
})
73+
})

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { useTemplateUrlLoader } from '@/platform/workflow/templates/composables/
1212
* - Input validation for template and source parameters
1313
*/
1414

15+
const preservedQueryMocks = vi.hoisted(() => ({
16+
clearPreservedQuery: vi.fn()
17+
}))
18+
1519
// Mock vue-router
1620
let mockQueryParams: Record<string, string | undefined> = {}
1721
const mockRouterReplace = vi.fn()
@@ -25,6 +29,11 @@ vi.mock('vue-router', () => ({
2529
}))
2630
}))
2731

32+
vi.mock(
33+
'@/platform/navigation/preservedQueryManager',
34+
() => preservedQueryMocks
35+
)
36+
2837
// Mock template workflows composable
2938
const mockLoadTemplates = vi.fn().mockResolvedValue(true)
3039
const mockLoadWorkflowTemplate = vi.fn().mockResolvedValue(true)
@@ -88,6 +97,7 @@ describe('useTemplateUrlLoader', () => {
8897
'flux_simple',
8998
'default'
9099
)
100+
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledTimes(1)
91101
})
92102

93103
it('uses default source when source param is not provided', async () => {

0 commit comments

Comments
 (0)