mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: convert download URLs to browsable page URLs in Copy Url button (#10228)
## Summary - Copy Url button in MissingModelRow now copies a browsable page URL instead of a direct download URL - HuggingFace: `/resolve/` → `/blob/` (file page with model info and download button) - Civitai: strips `/api/download` or `/api/v1` prefix (model page) ## Changes - Add `toBrowsableUrl()` to `missingModelDownload.ts` — converts download URLs to browsable page URLs for HuggingFace and Civitai - Update `MissingModelRow.vue` to use `toBrowsableUrl()` when copying - Add 5 unit tests covering HuggingFace, Civitai, and non-matching URL cases ## Test plan - [x] Unit tests pass (14/14) — `toBrowsableUrl` covered by 5 dedicated tests - [x] Lint, format, typecheck pass - [x] Manual: load workflow with missing HuggingFace models, click Copy Url, verify copied URL opens the file page - [x] Manual: load workflow with missing Civitai models, click Copy Url, verify copied URL opens the model page ### Why no E2E test The Copy Url button is only visible when `!isCloud && model.representative.url && !isAssetSupported`. Writing an E2E test for the clipboard content would require: 1. A test fixture with a real HuggingFace/Civitai URL (fragile — depends on external availability) 2. Granting `clipboard-read` permission in Playwright context 3. Ensuring `isAssetSupported` evaluates to `false` against the local test server's node definitions The URL transformation logic is a pure function, fully covered by unit tests. E2E clipboard content verification has no existing patterns in the codebase and would be environment-dependent and flaky. --------- Co-authored-by: Jin Yi <jin12cc@gmail.com>
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="h-8 shrink-0 rounded-lg text-sm"
|
||||
@click="copyToClipboard(model.representative.url!)"
|
||||
@click="copyToClipboard(toBrowsableUrl(model.representative.url!))"
|
||||
>
|
||||
{{ t('rightSidePanel.missingModels.copyUrl') }}
|
||||
</Button>
|
||||
@@ -201,7 +201,8 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import {
|
||||
downloadModel,
|
||||
isModelDownloadable
|
||||
isModelDownloadable,
|
||||
toBrowsableUrl
|
||||
} from '@/platform/missingModel/missingModelDownload'
|
||||
import { formatSize } from '@/utils/formatUtil'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { fetchModelMetadata } from './missingModelDownload'
|
||||
import { fetchModelMetadata, toBrowsableUrl } from './missingModelDownload'
|
||||
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
@@ -140,3 +140,41 @@ describe('fetchModelMetadata', () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toBrowsableUrl', () => {
|
||||
it('replaces /resolve/ with /blob/ in HuggingFace URLs', () => {
|
||||
expect(
|
||||
toBrowsableUrl(
|
||||
'https://huggingface.co/org/model/resolve/main/file.safetensors'
|
||||
)
|
||||
).toBe('https://huggingface.co/org/model/blob/main/file.safetensors')
|
||||
})
|
||||
|
||||
it('returns non-HuggingFace URLs unchanged', () => {
|
||||
const url =
|
||||
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
|
||||
expect(toBrowsableUrl(url)).toBe(url)
|
||||
})
|
||||
|
||||
it('preserves query params in HuggingFace URLs', () => {
|
||||
expect(
|
||||
toBrowsableUrl(
|
||||
'https://huggingface.co/bfl/FLUX.1/resolve/main/model.safetensors?download=true'
|
||||
)
|
||||
).toBe(
|
||||
'https://huggingface.co/bfl/FLUX.1/blob/main/model.safetensors?download=true'
|
||||
)
|
||||
})
|
||||
|
||||
it('converts Civitai api/download URL to model page', () => {
|
||||
expect(
|
||||
toBrowsableUrl('https://civitai.com/api/download/models/12345')
|
||||
).toBe('https://civitai.com/models/12345')
|
||||
})
|
||||
|
||||
it('converts Civitai api/v1 URL to model page', () => {
|
||||
expect(toBrowsableUrl('https://civitai.com/api/v1/models/12345')).toBe(
|
||||
'https://civitai.com/models/12345'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,6 +31,21 @@ interface ModelWithUrl {
|
||||
directory: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a model download URL to a browsable page URL.
|
||||
* - HuggingFace: `/resolve/` → `/blob/` (file page with model info)
|
||||
* - Civitai: strips `/api/download` or `/api/v1` prefix (model page)
|
||||
*/
|
||||
export function toBrowsableUrl(url: string): string {
|
||||
if (isCivitaiModelUrl(url)) {
|
||||
return url.replace('/api/download/', '/').replace('/api/v1/', '/')
|
||||
}
|
||||
if (url.includes('huggingface.co')) {
|
||||
return url.replace('/resolve/', '/blob/')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
export function isModelDownloadable(model: ModelWithUrl): boolean {
|
||||
if (WHITE_LISTED_URLS.has(model.url)) return true
|
||||
if (!ALLOWED_SOURCES.some((source) => model.url.startsWith(source)))
|
||||
|
||||
Reference in New Issue
Block a user