mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
fix(links): let CivitAI and HuggingFace links coexist (#1094)
A model could have CivitAI metadata and a HuggingFace link at the same time, but only one of the two "View on ..." entries ever rendered, because both the model modal and the card globe asked the `from_civitai` provenance flag which source to show. `set_hf_url` wrote `false` and a CivitAI refresh wrote `true`, so whichever ran last erased the other: linking HF hid "View on CivitAI" even though the civitai payload was still in the sidecar, and (on the card) a later refresh pointed the single globe icon back at CivitAI, hiding the HF entry. Decide the links from the data itself instead: - `set_hf_url` no longer touches `from_civitai`; it records where the metadata came from, and HF provenance is already tracked by `hf_url`. - Add `hasCivitaiSource(civitai)` in the shared card/modal utils and gate the modal's CivitAI link, the card globe (title, enabled state, click target, new `data-has_civitai`) and the context-menu `civitai` action on actual CivitAI data (`modelId` / `model_id` / `id`). A dual-source model now shows both links, and a CivitAI-only model with no `hf_url` stays as before. - Agent HF enrichment (`PostProcessor.is_hf_model`) keyed off `not from_civitai`, which stopped being a synonym for "has an HF source" once both sources can coexist (and already broke after a CivitAI refresh flipped the flag back to true). Key it off `hf_url` directly; the post-processor tests move to that discriminator and gain a dual-source case. Regression tests: the set-hf-url handler preserves civitai + `from_civitai` and no longer forces the flag false, the modal renders both links (including with `from_civitai: false`), and the card globe targets/opens the right source and is disabled when neither is available. Backend: 2749 passed. Frontend: 1098 JS + 91 Vue tests passed.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
const {
|
||||
MODEL_CARD_MODULE,
|
||||
STATE_MODULE,
|
||||
UI_HELPERS_MODULE,
|
||||
I18N_MODULE,
|
||||
API_CONFIG_MODULE,
|
||||
API_FACTORY_MODULE,
|
||||
} = vi.hoisted(() => ({
|
||||
MODEL_CARD_MODULE: new URL('../../../static/js/components/shared/ModelCard.js', import.meta.url).pathname,
|
||||
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
|
||||
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||
I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
||||
API_CONFIG_MODULE: new URL('../../../static/js/api/apiConfig.js', import.meta.url).pathname,
|
||||
API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname,
|
||||
}));
|
||||
|
||||
vi.mock(STATE_MODULE, () => ({
|
||||
state: {
|
||||
settings: {
|
||||
blur_mature_content: false,
|
||||
model_name_display: 'model_name',
|
||||
},
|
||||
global: {
|
||||
settings: {
|
||||
model_name_display: 'model_name',
|
||||
group_by_model: false,
|
||||
display_density: 'default',
|
||||
model_card_footer_action: 'example_images',
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
other: {
|
||||
previewVersions: new Map(),
|
||||
sortBy: 'name',
|
||||
},
|
||||
},
|
||||
bulkMode: false,
|
||||
selectedModels: new Set(),
|
||||
selectedLoras: new Set(),
|
||||
},
|
||||
getCurrentPageState: vi.fn(() => ({
|
||||
sortBy: 'name',
|
||||
previewVersions: new Map(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||
showToast: vi.fn(),
|
||||
openCivitai: vi.fn(),
|
||||
openHuggingFace: vi.fn(),
|
||||
copyToClipboard: vi.fn(),
|
||||
copyLoraSyntax: vi.fn(),
|
||||
sendLoraToWorkflow: vi.fn(),
|
||||
sendEmbeddingToWorkflow: vi.fn(),
|
||||
openExampleImagesFolder: vi.fn(),
|
||||
buildLoraSyntax: vi.fn(),
|
||||
sendModelPathToWorkflow: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock(I18N_MODULE, () => ({
|
||||
translate: vi.fn((key, params, fallback) => (typeof fallback === 'string' ? fallback : key)),
|
||||
}));
|
||||
|
||||
vi.mock(API_CONFIG_MODULE, () => ({
|
||||
MODEL_TYPES: { LORA: 'loras', CHECKPOINT: 'checkpoints', EMBEDDING: 'embeddings', OTHER: 'other' },
|
||||
}));
|
||||
|
||||
vi.mock(API_FACTORY_MODULE, () => ({
|
||||
getModelApiClient: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
function makeModel(overrides = {}) {
|
||||
return {
|
||||
sha256: 'abc123',
|
||||
file_path: '/models/loras/linked.safetensors',
|
||||
model_name: 'Linked LoRA',
|
||||
file_name: 'linked',
|
||||
folder: '',
|
||||
modified: 1234567890,
|
||||
file_size: 1024,
|
||||
notes: '',
|
||||
base_model: '',
|
||||
favorite: false,
|
||||
exclude: false,
|
||||
from_civitai: true,
|
||||
hf_url: '',
|
||||
update_available: false,
|
||||
skip_metadata_refresh: false,
|
||||
preview_url: '',
|
||||
preview_nsfw_level: 0,
|
||||
tags: [],
|
||||
civitai: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function mountCard(createModelCard, model) {
|
||||
document.body.innerHTML = '<div id="modelGrid"></div>';
|
||||
const card = createModelCard(model, 'loras');
|
||||
document.getElementById('modelGrid').appendChild(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
describe('ModelCard source globe (#1094)', () => {
|
||||
let createModelCard;
|
||||
let setupModelCardEventDelegation;
|
||||
let openCivitai;
|
||||
let openHuggingFace;
|
||||
|
||||
beforeEach(async () => {
|
||||
document.body.innerHTML = '';
|
||||
({ createModelCard, setupModelCardEventDelegation } = await import(MODEL_CARD_MODULE));
|
||||
({ openCivitai, openHuggingFace } = await import(UI_HELPERS_MODULE));
|
||||
openCivitai.mockReset();
|
||||
openHuggingFace.mockReset();
|
||||
});
|
||||
|
||||
it('points the globe at CivitAI when CivitAI data is present alongside an HF link', () => {
|
||||
const card = mountCard(
|
||||
createModelCard,
|
||||
makeModel({
|
||||
civitai: { id: 111, modelId: 222, name: 'v1' },
|
||||
hf_url: 'https://huggingface.co/user/repo',
|
||||
})
|
||||
);
|
||||
|
||||
expect(card.dataset.has_civitai).toBe('true');
|
||||
expect(card.querySelector('.fa-globe').getAttribute('title')).toBe('View on Civitai');
|
||||
});
|
||||
|
||||
it('keeps the CivitAI globe target when from_civitai is false but CivitAI data exists', () => {
|
||||
// Regression for case 1: linking HF no longer hides CivitAI.
|
||||
const card = mountCard(
|
||||
createModelCard,
|
||||
makeModel({
|
||||
from_civitai: false,
|
||||
civitai: { id: 111, modelId: 222 },
|
||||
hf_url: 'https://huggingface.co/user/repo',
|
||||
})
|
||||
);
|
||||
|
||||
expect(card.dataset.has_civitai).toBe('true');
|
||||
expect(card.querySelector('.fa-globe').getAttribute('title')).toBe('View on Civitai');
|
||||
});
|
||||
|
||||
it('points the globe at HuggingFace for an HF-only model', () => {
|
||||
const card = mountCard(
|
||||
createModelCard,
|
||||
makeModel({ from_civitai: false, civitai: {}, hf_url: 'https://huggingface.co/user/repo' })
|
||||
);
|
||||
|
||||
expect(card.dataset.has_civitai).toBe('false');
|
||||
expect(card.querySelector('.fa-globe').getAttribute('title')).toBe('View on Hugging Face');
|
||||
});
|
||||
|
||||
it('disables the globe when there is no CivitAI data and no HF link', () => {
|
||||
const card = mountCard(createModelCard, makeModel({ civitai: {} }));
|
||||
const globe = card.querySelector('.fa-globe');
|
||||
|
||||
expect(card.dataset.has_civitai).toBe('false');
|
||||
expect(globe.getAttribute('style')).toContain('cursor: not-allowed');
|
||||
});
|
||||
|
||||
it('opens CivitAI when the globe is clicked on a dual-source model', () => {
|
||||
const model = makeModel({
|
||||
civitai: { id: 111, modelId: 222 },
|
||||
hf_url: 'https://huggingface.co/user/repo',
|
||||
});
|
||||
const card = mountCard(createModelCard, model);
|
||||
setupModelCardEventDelegation('loras');
|
||||
|
||||
card.querySelector('.fa-globe').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
|
||||
expect(openCivitai).toHaveBeenCalledWith(model.file_path);
|
||||
expect(openHuggingFace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens HuggingFace when the globe is clicked on an HF-only model', () => {
|
||||
const model = makeModel({
|
||||
from_civitai: false,
|
||||
civitai: {},
|
||||
hf_url: 'https://huggingface.co/user/repo',
|
||||
});
|
||||
const card = mountCard(createModelCard, model);
|
||||
setupModelCardEventDelegation('loras');
|
||||
|
||||
card.querySelector('.fa-globe').dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
|
||||
expect(openHuggingFace).toHaveBeenCalledWith('https://huggingface.co/user/repo');
|
||||
expect(openCivitai).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user