mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-17 17:28:58 +00:00
Compare commits
3 Commits
split/allo
...
matt/be-22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1e7ab4447 | ||
|
|
c4e81eaf93 | ||
|
|
e5d68349f5 |
@@ -0,0 +1,120 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { usePreservedQueryDeepLink } from './usePreservedQueryDeepLink'
|
||||
|
||||
const preservedQueryMocks = vi.hoisted(() => ({
|
||||
clearPreservedQuery: vi.fn(),
|
||||
hydratePreservedQuery: vi.fn(),
|
||||
mergePreservedQueryIntoQuery: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/platform/navigation/preservedQueryManager',
|
||||
() => preservedQueryMocks
|
||||
)
|
||||
|
||||
const mockRouteQuery = vi.hoisted(() => ({
|
||||
value: {} as Record<string, string>
|
||||
}))
|
||||
const mockRouterReplace = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({
|
||||
query: mockRouteQuery.value
|
||||
}),
|
||||
useRouter: () => ({
|
||||
replace: mockRouterReplace
|
||||
})
|
||||
}))
|
||||
|
||||
describe('usePreservedQueryDeepLink', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockRouteQuery.value = {}
|
||||
preservedQueryMocks.mergePreservedQueryIntoQuery.mockReturnValue(null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('hydrateAndRead', () => {
|
||||
it('merges a preserved value into the URL and returns it', async () => {
|
||||
preservedQueryMocks.mergePreservedQueryIntoQuery.mockReturnValue({
|
||||
invite: 'preserved-token'
|
||||
})
|
||||
|
||||
const { hydrateAndRead } = usePreservedQueryDeepLink('invite', 'invite')
|
||||
const value = await hydrateAndRead()
|
||||
|
||||
expect(preservedQueryMocks.hydratePreservedQuery).toHaveBeenCalledWith(
|
||||
'invite'
|
||||
)
|
||||
expect(mockRouterReplace).toHaveBeenCalledWith({
|
||||
query: { invite: 'preserved-token' }
|
||||
})
|
||||
expect(value).toBe('preserved-token')
|
||||
})
|
||||
|
||||
it('returns the current route value without replacing when nothing is preserved', async () => {
|
||||
mockRouteQuery.value = { invite: 'from-url' }
|
||||
|
||||
const { hydrateAndRead } = usePreservedQueryDeepLink('invite', 'invite')
|
||||
const value = await hydrateAndRead()
|
||||
|
||||
expect(preservedQueryMocks.hydratePreservedQuery).toHaveBeenCalledWith(
|
||||
'invite'
|
||||
)
|
||||
expect(mockRouterReplace).not.toHaveBeenCalled()
|
||||
expect(value).toBe('from-url')
|
||||
})
|
||||
})
|
||||
|
||||
describe('strip', () => {
|
||||
it('removes the key from the URL and clears the preserved namespace', () => {
|
||||
mockRouteQuery.value = { invite: 'from-url', other: 'keep' }
|
||||
|
||||
const { strip } = usePreservedQueryDeepLink('invite', 'invite')
|
||||
strip()
|
||||
|
||||
expect(mockRouterReplace).toHaveBeenCalledWith({
|
||||
query: { other: 'keep' }
|
||||
})
|
||||
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
|
||||
'invite'
|
||||
)
|
||||
})
|
||||
|
||||
it('clears the preserved namespace without replacing when the key is absent', () => {
|
||||
mockRouteQuery.value = { other: 'keep' }
|
||||
|
||||
const { strip } = usePreservedQueryDeepLink('invite', 'invite')
|
||||
strip()
|
||||
|
||||
expect(mockRouterReplace).not.toHaveBeenCalled()
|
||||
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
|
||||
'invite'
|
||||
)
|
||||
})
|
||||
|
||||
it('logs a warning when cleaning the URL param fails', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const replaceError = new Error('navigation guard rejected')
|
||||
mockRouterReplace.mockRejectedValueOnce(replaceError)
|
||||
mockRouteQuery.value = { invite: 'from-url' }
|
||||
|
||||
const { strip } = usePreservedQueryDeepLink('invite', 'invite')
|
||||
strip()
|
||||
await vi.waitFor(() =>
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('invite'),
|
||||
replaceError
|
||||
)
|
||||
)
|
||||
|
||||
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
|
||||
'invite'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { LocationQuery } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
clearPreservedQuery,
|
||||
hydratePreservedQuery,
|
||||
mergePreservedQueryIntoQuery
|
||||
} from '@/platform/navigation/preservedQueryManager'
|
||||
|
||||
/**
|
||||
* Shared head/tail for URL deep-link loaders that survive a login redirect via
|
||||
* the preserved-query (sessionStorage) system. Extracts ONLY the boilerplate:
|
||||
* - hydrateAndRead(): restore the preserved param, reflect it in the URL, and
|
||||
* return the value of `key` from the (possibly merged) query.
|
||||
* - strip(): remove `key` from the URL and clear the preserved namespace.
|
||||
* Each caller keeps its OWN gate/validation and action explicit — this helper
|
||||
* intentionally contains no auth logic.
|
||||
*/
|
||||
export function usePreservedQueryDeepLink(namespace: string, key: string) {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const hydrateAndRead = async (): Promise<LocationQuery[string]> => {
|
||||
hydratePreservedQuery(namespace)
|
||||
const merged = mergePreservedQueryIntoQuery(namespace, route.query)
|
||||
if (merged) {
|
||||
await router.replace({ query: merged })
|
||||
}
|
||||
return (merged ?? route.query)[key] as LocationQuery[string]
|
||||
}
|
||||
|
||||
const strip = () => {
|
||||
if (key in route.query) {
|
||||
const newQuery = { ...route.query }
|
||||
delete newQuery[key]
|
||||
router.replace({ query: newQuery }).catch((error) => {
|
||||
console.warn(
|
||||
`[usePreservedQueryDeepLink] Failed to clean URL param "${key}":`,
|
||||
error
|
||||
)
|
||||
})
|
||||
}
|
||||
clearPreservedQuery(namespace)
|
||||
}
|
||||
|
||||
return { hydrateAndRead, strip }
|
||||
}
|
||||
@@ -195,6 +195,7 @@ function createDeferred() {
|
||||
describe('useSharedWorkflowUrlLoader', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
mockRouterReplace.mockResolvedValue(undefined)
|
||||
mockQueryParams = {}
|
||||
mockIsLoggedIn.value = false
|
||||
mockDialogStack.length = 0
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { usePreservedQueryDeepLink } from '@/platform/navigation/composables/usePreservedQueryDeepLink'
|
||||
import OpenSharedWorkflowDialogContent from '@/platform/workflow/sharing/components/OpenSharedWorkflowDialogContent.vue'
|
||||
import type { SharedWorkflowPayload } from '@/platform/workflow/sharing/types/shareTypes'
|
||||
import {
|
||||
clearPreservedQuery,
|
||||
hydratePreservedQuery,
|
||||
mergePreservedQueryIntoQuery
|
||||
} from '@/platform/navigation/preservedQueryManager'
|
||||
import { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'
|
||||
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
|
||||
import { useWorkflowShareService } from '@/platform/workflow/sharing/services/workflowShareService'
|
||||
import { isValidShareId } from '@/platform/workflow/sharing/utils/shareAuthAttribution'
|
||||
@@ -37,8 +33,6 @@ type OpeningAction = Exclude<DialogResult['action'], 'cancel'>
|
||||
const OPEN_SHARED_WORKFLOW_DIALOG_KEY = 'open-shared-workflow'
|
||||
|
||||
export function useSharedWorkflowUrlLoader() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const workflowShareService = useWorkflowShareService()
|
||||
@@ -48,31 +42,9 @@ export function useSharedWorkflowUrlLoader() {
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const { mode, isAppMode } = useAppMode()
|
||||
const SHARE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.SHARE
|
||||
const deepLink = usePreservedQueryDeepLink(SHARE_NAMESPACE, 'share')
|
||||
|
||||
async function ensureShareQueryFromIntent() {
|
||||
hydratePreservedQuery(SHARE_NAMESPACE)
|
||||
const mergedQuery = mergePreservedQueryIntoQuery(
|
||||
SHARE_NAMESPACE,
|
||||
route.query
|
||||
)
|
||||
|
||||
if (mergedQuery) {
|
||||
await router.replace({ query: mergedQuery })
|
||||
}
|
||||
|
||||
return mergedQuery ?? route.query
|
||||
}
|
||||
|
||||
function cleanupUrlParams() {
|
||||
const newQuery = { ...route.query }
|
||||
delete newQuery.share
|
||||
void router.replace({ query: newQuery })
|
||||
}
|
||||
|
||||
function clearShareIntent() {
|
||||
cleanupUrlParams()
|
||||
clearPreservedQuery(SHARE_NAMESPACE)
|
||||
}
|
||||
const clearShareIntent = () => deepLink.strip()
|
||||
|
||||
function showOpenSharedWorkflowDialog(
|
||||
shareId: string
|
||||
@@ -113,8 +85,7 @@ export function useSharedWorkflowUrlLoader() {
|
||||
}
|
||||
|
||||
async function loadSharedWorkflowFromUrl(): Promise<SharedWorkflowUrlLoadStatus> {
|
||||
const query = await ensureShareQueryFromIntent()
|
||||
const shareParam = query.share
|
||||
const shareParam = await deepLink.hydrateAndRead()
|
||||
|
||||
if (shareParam == null) {
|
||||
return 'not-present'
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
clearPreservedQuery,
|
||||
hydratePreservedQuery,
|
||||
mergePreservedQueryIntoQuery
|
||||
} from '@/platform/navigation/preservedQueryManager'
|
||||
import { usePreservedQueryDeepLink } from '@/platform/navigation/composables/usePreservedQueryDeepLink'
|
||||
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
@@ -21,9 +15,8 @@ const NAMESPACE = PRESERVED_QUERY_NAMESPACES.CREATE_WORKSPACE
|
||||
* as the invite URL loader.
|
||||
*/
|
||||
export function useCreateWorkspaceUrlLoader() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const dialogService = useDialogService()
|
||||
const deepLink = usePreservedQueryDeepLink(NAMESPACE, 'create_workspace')
|
||||
|
||||
/**
|
||||
* Opens the team workspaces dialog if `?create_workspace=1` is present.
|
||||
@@ -35,25 +28,10 @@ export function useCreateWorkspaceUrlLoader() {
|
||||
* 4. Clean up URL and preserved query
|
||||
*/
|
||||
async function loadCreateWorkspaceFromUrl() {
|
||||
hydratePreservedQuery(NAMESPACE)
|
||||
const mergedQuery = mergePreservedQueryIntoQuery(NAMESPACE, route.query)
|
||||
if (mergedQuery) {
|
||||
await router.replace({ query: mergedQuery })
|
||||
}
|
||||
|
||||
const query = mergedQuery ?? route.query
|
||||
const param = query.create_workspace
|
||||
const param = await deepLink.hydrateAndRead()
|
||||
if (!param || typeof param !== 'string') return
|
||||
|
||||
const newQuery = { ...route.query }
|
||||
delete newQuery.create_workspace
|
||||
router.replace({ query: newQuery }).catch((error) => {
|
||||
console.warn(
|
||||
'[useCreateWorkspaceUrlLoader] Failed to clean URL params:',
|
||||
error
|
||||
)
|
||||
})
|
||||
clearPreservedQuery(NAMESPACE)
|
||||
deepLink.strip()
|
||||
|
||||
try {
|
||||
await dialogService.showTeamWorkspacesDialog()
|
||||
|
||||
@@ -28,7 +28,7 @@ vi.mock(
|
||||
const mockRouteQuery = vi.hoisted(() => ({
|
||||
value: {} as Record<string, string>
|
||||
}))
|
||||
const mockRouterReplace = vi.hoisted(() => vi.fn())
|
||||
const mockRouterReplace = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import {
|
||||
clearPreservedQuery,
|
||||
hydratePreservedQuery,
|
||||
mergePreservedQueryIntoQuery
|
||||
} from '@/platform/navigation/preservedQueryManager'
|
||||
import { usePreservedQueryDeepLink } from '@/platform/navigation/composables/usePreservedQueryDeepLink'
|
||||
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
|
||||
|
||||
import { useTeamWorkspaceStore } from '../stores/teamWorkspaceStore'
|
||||
@@ -22,39 +17,11 @@ import { useTeamWorkspaceStore } from '../stores/teamWorkspaceStore'
|
||||
* as the template URL loader.
|
||||
*/
|
||||
export function useInviteUrlLoader() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const INVITE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.INVITE
|
||||
|
||||
/**
|
||||
* Hydrates preserved query from sessionStorage and merges into route.
|
||||
* This restores the invite token after login redirects.
|
||||
*/
|
||||
const ensureInviteQueryFromIntent = async () => {
|
||||
hydratePreservedQuery(INVITE_NAMESPACE)
|
||||
const mergedQuery = mergePreservedQueryIntoQuery(
|
||||
INVITE_NAMESPACE,
|
||||
route.query
|
||||
)
|
||||
|
||||
if (mergedQuery) {
|
||||
await router.replace({ query: mergedQuery })
|
||||
}
|
||||
|
||||
return mergedQuery ?? route.query
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes invite parameter from URL using Vue Router
|
||||
*/
|
||||
const cleanupUrlParams = () => {
|
||||
const newQuery = { ...route.query }
|
||||
delete newQuery.invite
|
||||
void router.replace({ query: newQuery })
|
||||
}
|
||||
const deepLink = usePreservedQueryDeepLink(INVITE_NAMESPACE, 'invite')
|
||||
|
||||
/**
|
||||
* Loads and accepts workspace invite from URL query parameters if present.
|
||||
@@ -68,9 +35,7 @@ export function useInviteUrlLoader() {
|
||||
* 5. Clean up URL and preserved query
|
||||
*/
|
||||
const loadInviteFromUrl = async () => {
|
||||
// Restore preserved query from sessionStorage (handles login redirect case)
|
||||
const query = await ensureInviteQueryFromIntent()
|
||||
const inviteParam = query.invite
|
||||
const inviteParam = await deepLink.hydrateAndRead()
|
||||
if (!inviteParam || typeof inviteParam !== 'string') {
|
||||
return
|
||||
}
|
||||
@@ -100,8 +65,7 @@ export function useInviteUrlLoader() {
|
||||
detail: error instanceof Error ? error.message : t('g.unknownError')
|
||||
})
|
||||
} finally {
|
||||
cleanupUrlParams()
|
||||
clearPreservedQuery(INVITE_NAMESPACE)
|
||||
deepLink.strip()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user