mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat(versions): add base filter toggle UI and styling
Add CSS classes and JavaScript logic for the base filter toggle button in the versions toolbar. The filter allows users to switch between showing all versions or only versions matching the current base model. Includes styling for different states (active, hover, disabled) and accessibility features like screen reader support.
This commit is contained in:
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "Alle lokalen Versionen anzeigen",
|
"viewLocalVersions": "Alle lokalen Versionen anzeigen",
|
||||||
"viewLocalTooltip": "Demnächst verfügbar"
|
"viewLocalTooltip": "Demnächst verfügbar"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Basisfilter",
|
||||||
|
"state": {
|
||||||
|
"showAll": "Alle Versionen",
|
||||||
|
"showSameBase": "Gleiches Basismodell"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "Wechseln, um alle Versionen anzuzeigen",
|
||||||
|
"showSameBaseVersions": "Wechseln, um nur Versionen mit demselben Basismodell anzuzeigen"
|
||||||
|
},
|
||||||
|
"empty": "Keine Versionen entsprechen dem Filter für das aktuelle Basismodell."
|
||||||
|
},
|
||||||
"empty": "Noch keine Versionshistorie für dieses Modell vorhanden.",
|
"empty": "Noch keine Versionshistorie für dieses Modell vorhanden.",
|
||||||
"error": "Versionen konnten nicht geladen werden.",
|
"error": "Versionen konnten nicht geladen werden.",
|
||||||
"missingModelId": "Für dieses Modell ist keine Civitai-Model-ID vorhanden.",
|
"missingModelId": "Für dieses Modell ist keine Civitai-Model-ID vorhanden.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "View all local versions",
|
"viewLocalVersions": "View all local versions",
|
||||||
"viewLocalTooltip": "Coming soon"
|
"viewLocalTooltip": "Coming soon"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Base filter",
|
||||||
|
"state": {
|
||||||
|
"showAll": "All versions",
|
||||||
|
"showSameBase": "Same base"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "Switch to showing all versions",
|
||||||
|
"showSameBaseVersions": "Switch to showing only versions that match the current base model"
|
||||||
|
},
|
||||||
|
"empty": "No versions match the current base model filter."
|
||||||
|
},
|
||||||
"empty": "No version history available for this model yet.",
|
"empty": "No version history available for this model yet.",
|
||||||
"error": "Failed to load versions.",
|
"error": "Failed to load versions.",
|
||||||
"missingModelId": "This model is missing a Civitai model id.",
|
"missingModelId": "This model is missing a Civitai model id.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "Ver todas las versiones locales",
|
"viewLocalVersions": "Ver todas las versiones locales",
|
||||||
"viewLocalTooltip": "Disponible pronto"
|
"viewLocalTooltip": "Disponible pronto"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filtro base",
|
||||||
|
"state": {
|
||||||
|
"showAll": "Todas las versiones",
|
||||||
|
"showSameBase": "Mismo modelo base"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "Cambiar para mostrar todas las versiones",
|
||||||
|
"showSameBaseVersions": "Cambiar para mostrar solo versiones del mismo modelo base"
|
||||||
|
},
|
||||||
|
"empty": "Ninguna versión coincide con el filtro del modelo base actual."
|
||||||
|
},
|
||||||
"empty": "Aún no hay historial de versiones para este modelo.",
|
"empty": "Aún no hay historial de versiones para este modelo.",
|
||||||
"error": "No se pudieron cargar las versiones.",
|
"error": "No se pudieron cargar las versiones.",
|
||||||
"missingModelId": "Este modelo no tiene un ID de modelo de Civitai.",
|
"missingModelId": "Este modelo no tiene un ID de modelo de Civitai.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "Voir toutes les versions locales",
|
"viewLocalVersions": "Voir toutes les versions locales",
|
||||||
"viewLocalTooltip": "Bientôt disponible"
|
"viewLocalTooltip": "Bientôt disponible"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filtre de base",
|
||||||
|
"state": {
|
||||||
|
"showAll": "Toutes les versions",
|
||||||
|
"showSameBase": "Même modèle de base"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "Passer à l'affichage de toutes les versions",
|
||||||
|
"showSameBaseVersions": "Passer à l'affichage des versions du même modèle de base"
|
||||||
|
},
|
||||||
|
"empty": "Aucune version ne correspond au filtre du modèle de base actuel."
|
||||||
|
},
|
||||||
"empty": "Aucun historique de versions n'est disponible pour ce modèle pour le moment.",
|
"empty": "Aucun historique de versions n'est disponible pour ce modèle pour le moment.",
|
||||||
"error": "Échec du chargement des versions.",
|
"error": "Échec du chargement des versions.",
|
||||||
"missingModelId": "Ce modèle ne possède pas d'identifiant de modèle Civitai.",
|
"missingModelId": "Ce modèle ne possède pas d'identifiant de modèle Civitai.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "הצג את כל הגרסאות המקומיות",
|
"viewLocalVersions": "הצג את כל הגרסאות המקומיות",
|
||||||
"viewLocalTooltip": "יגיע בקרוב"
|
"viewLocalTooltip": "יגיע בקרוב"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "מסנן בסיס",
|
||||||
|
"state": {
|
||||||
|
"showAll": "כל הגרסאות",
|
||||||
|
"showSameBase": "אותו מודל בסיס"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "החלף להצגת כל הגרסאות",
|
||||||
|
"showSameBaseVersions": "החלף להצגת גרסאות עם אותו מודל בסיס"
|
||||||
|
},
|
||||||
|
"empty": "אין גרסאות התואמות את המסנן של מודל הבסיס הנוכחי."
|
||||||
|
},
|
||||||
"empty": "אין עדיין היסטוריית גרסאות למודל זה.",
|
"empty": "אין עדיין היסטוריית גרסאות למודל זה.",
|
||||||
"error": "טעינת הגרסאות נכשלה.",
|
"error": "טעינת הגרסאות נכשלה.",
|
||||||
"missingModelId": "למודל זה אין מזהה מודל של Civitai.",
|
"missingModelId": "למודל זה אין מזהה מודל של Civitai.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "ローカルの全バージョンを表示",
|
"viewLocalVersions": "ローカルの全バージョンを表示",
|
||||||
"viewLocalTooltip": "近日対応予定"
|
"viewLocalTooltip": "近日対応予定"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "ベースフィルター",
|
||||||
|
"state": {
|
||||||
|
"showAll": "すべてのバージョン",
|
||||||
|
"showSameBase": "同じベース"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "すべてのバージョンを表示する",
|
||||||
|
"showSameBaseVersions": "同じベースモデルのバージョンのみ表示する"
|
||||||
|
},
|
||||||
|
"empty": "現在のベースモデルフィルターに一致するバージョンがありません。"
|
||||||
|
},
|
||||||
"empty": "このモデルにはまだバージョン履歴がありません。",
|
"empty": "このモデルにはまだバージョン履歴がありません。",
|
||||||
"error": "バージョンの読み込みに失敗しました。",
|
"error": "バージョンの読み込みに失敗しました。",
|
||||||
"missingModelId": "このモデルにはCivitaiのモデルIDがありません。",
|
"missingModelId": "このモデルにはCivitaiのモデルIDがありません。",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "로컬 버전 모두 보기",
|
"viewLocalVersions": "로컬 버전 모두 보기",
|
||||||
"viewLocalTooltip": "곧 제공 예정"
|
"viewLocalTooltip": "곧 제공 예정"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "기본 필터",
|
||||||
|
"state": {
|
||||||
|
"showAll": "모든 버전",
|
||||||
|
"showSameBase": "같은 베이스"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "모든 버전을 표시하도록 전환",
|
||||||
|
"showSameBaseVersions": "같은 베이스 모델 버전만 표시하도록 전환"
|
||||||
|
},
|
||||||
|
"empty": "현재 베이스 모델 필터와 일치하는 버전이 없습니다."
|
||||||
|
},
|
||||||
"empty": "이 모델에는 아직 버전 기록이 없습니다.",
|
"empty": "이 모델에는 아직 버전 기록이 없습니다.",
|
||||||
"error": "버전을 불러오지 못했습니다.",
|
"error": "버전을 불러오지 못했습니다.",
|
||||||
"missingModelId": "이 모델에는 Civitai 모델 ID가 없습니다.",
|
"missingModelId": "이 모델에는 Civitai 모델 ID가 없습니다.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "Показать все локальные версии",
|
"viewLocalVersions": "Показать все локальные версии",
|
||||||
"viewLocalTooltip": "Скоро появится"
|
"viewLocalTooltip": "Скоро появится"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Фильтр по базе",
|
||||||
|
"state": {
|
||||||
|
"showAll": "Все версии",
|
||||||
|
"showSameBase": "Тот же базовый"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "Переключиться на отображение всех версий",
|
||||||
|
"showSameBaseVersions": "Переключиться на отображение только версий с тем же базовым"
|
||||||
|
},
|
||||||
|
"empty": "Нет версий, соответствующих текущему фильтру базовой модели."
|
||||||
|
},
|
||||||
"empty": "Для этой модели пока нет истории версий.",
|
"empty": "Для этой модели пока нет истории версий.",
|
||||||
"error": "Не удалось загрузить версии.",
|
"error": "Не удалось загрузить версии.",
|
||||||
"missingModelId": "У этой модели отсутствует идентификатор модели Civitai.",
|
"missingModelId": "У этой модели отсутствует идентификатор модели Civitai.",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "查看所有本地版本",
|
"viewLocalVersions": "查看所有本地版本",
|
||||||
"viewLocalTooltip": "敬请期待"
|
"viewLocalTooltip": "敬请期待"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "基础筛选",
|
||||||
|
"state": {
|
||||||
|
"showAll": "全部版本",
|
||||||
|
"showSameBase": "相同基模型"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "切换为显示所有版本",
|
||||||
|
"showSameBaseVersions": "仅显示与当前基模型匹配的版本"
|
||||||
|
},
|
||||||
|
"empty": "没有与当前基模型筛选匹配的版本。"
|
||||||
|
},
|
||||||
"empty": "该模型还没有版本历史。",
|
"empty": "该模型还没有版本历史。",
|
||||||
"error": "加载版本失败。",
|
"error": "加载版本失败。",
|
||||||
"missingModelId": "该模型缺少 Civitai 模型 ID。",
|
"missingModelId": "该模型缺少 Civitai 模型 ID。",
|
||||||
|
|||||||
@@ -938,6 +938,18 @@
|
|||||||
"viewLocalVersions": "檢視所有本地版本",
|
"viewLocalVersions": "檢視所有本地版本",
|
||||||
"viewLocalTooltip": "敬請期待"
|
"viewLocalTooltip": "敬請期待"
|
||||||
},
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "基礎篩選",
|
||||||
|
"state": {
|
||||||
|
"showAll": "所有版本",
|
||||||
|
"showSameBase": "相同基礎模型"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"showAllVersions": "切換為顯示所有版本",
|
||||||
|
"showSameBaseVersions": "僅顯示與目前基礎模型相符的版本"
|
||||||
|
},
|
||||||
|
"empty": "沒有符合目前基礎模型篩選的版本。"
|
||||||
|
},
|
||||||
"empty": "此模型尚無版本歷史。",
|
"empty": "此模型尚無版本歷史。",
|
||||||
"error": "載入版本失敗。",
|
"error": "載入版本失敗。",
|
||||||
"missingModelId": "此模型缺少 Civitai 模型 ID。",
|
"missingModelId": "此模型缺少 Civitai 模型 ID。",
|
||||||
|
|||||||
@@ -24,12 +24,29 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.versions-toolbar-info p {
|
.versions-toolbar-info p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.versions-toolbar-info-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
.versions-toolbar-actions {
|
.versions-toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -68,6 +85,41 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.versions-filter-toggle {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
background: color-mix(in oklch, var(--card-bg) 80%, var(--bg-color));
|
||||||
|
align-self: center;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-filter-toggle i {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-filter-toggle:hover:not(:disabled) {
|
||||||
|
border-color: var(--text-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-filter-toggle[data-filter-active="true"] {
|
||||||
|
border-color: color-mix(in oklch, var(--lora-accent) 65%, transparent);
|
||||||
|
color: var(--lora-accent);
|
||||||
|
background: color-mix(in oklch, var(--lora-accent) 20%, var(--card-bg) 80%);
|
||||||
|
}
|
||||||
|
|
||||||
.versions-toolbar-btn:disabled {
|
.versions-toolbar-btn:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -152,6 +152,81 @@ function buildBadge(label, tone) {
|
|||||||
return `<span class="version-badge version-badge-${tone}">${escapeHtml(label)}</span>`;
|
return `<span class="version-badge version-badge-${tone}">${escapeHtml(label)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DISPLAY_FILTER_MODES = Object.freeze({
|
||||||
|
SAME_BASE: 'same_base',
|
||||||
|
ANY: 'any',
|
||||||
|
});
|
||||||
|
|
||||||
|
const FILTER_LABEL_KEY = 'modals.model.versions.filters.label';
|
||||||
|
const FILTER_STATE_KEYS = {
|
||||||
|
[DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.state.showSameBase',
|
||||||
|
[DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.state.showAll',
|
||||||
|
};
|
||||||
|
const FILTER_TOOLTIP_KEYS = {
|
||||||
|
[DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.tooltip.showAllVersions',
|
||||||
|
[DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.tooltip.showSameBaseVersions',
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeBaseModelName(value) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return trimmed.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToggleLabelText() {
|
||||||
|
return translate(FILTER_LABEL_KEY, {}, 'Base filter');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToggleStateText(mode) {
|
||||||
|
const key = FILTER_STATE_KEYS[mode] || FILTER_STATE_KEYS[DISPLAY_FILTER_MODES.ANY];
|
||||||
|
const fallback =
|
||||||
|
mode === DISPLAY_FILTER_MODES.SAME_BASE ? 'Same base' : 'All versions';
|
||||||
|
return translate(key, {}, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToggleTooltipText(mode) {
|
||||||
|
const key =
|
||||||
|
FILTER_TOOLTIP_KEYS[mode] || FILTER_TOOLTIP_KEYS[DISPLAY_FILTER_MODES.ANY];
|
||||||
|
const fallback =
|
||||||
|
mode === DISPLAY_FILTER_MODES.SAME_BASE
|
||||||
|
? 'Switch to showing all versions'
|
||||||
|
: 'Switch to showing only versions with the current base model';
|
||||||
|
return translate(key, {}, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultDisplayMode() {
|
||||||
|
const strategy = state?.global?.settings?.update_flag_strategy;
|
||||||
|
return strategy === DISPLAY_FILTER_MODES.SAME_BASE
|
||||||
|
? DISPLAY_FILTER_MODES.SAME_BASE
|
||||||
|
: DISPLAY_FILTER_MODES.ANY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentVersionBaseModel(record, versionId) {
|
||||||
|
if (!record || typeof versionId !== 'number' || !Array.isArray(record.versions)) {
|
||||||
|
return {
|
||||||
|
normalized: null,
|
||||||
|
raw: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const currentVersion = record.versions.find(v => v.versionId === versionId);
|
||||||
|
if (!currentVersion) {
|
||||||
|
return {
|
||||||
|
normalized: null,
|
||||||
|
raw: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const baseModelRaw = currentVersion.baseModel ?? null;
|
||||||
|
return {
|
||||||
|
normalized: normalizeBaseModelName(baseModelRaw),
|
||||||
|
raw: baseModelRaw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getAutoplaySetting() {
|
function getAutoplaySetting() {
|
||||||
try {
|
try {
|
||||||
return Boolean(state?.global?.settings?.autoplay_on_hover);
|
return Boolean(state?.global?.settings?.autoplay_on_hover);
|
||||||
@@ -314,7 +389,7 @@ function getLatestLibraryVersionId(record) {
|
|||||||
return Math.max(...record.inLibraryVersionIds);
|
return Math.max(...record.inLibraryVersionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderToolbar(record) {
|
function renderToolbar(record, toolbarState = {}) {
|
||||||
const ignoreText = record.shouldIgnore
|
const ignoreText = record.shouldIgnore
|
||||||
? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model')
|
? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model')
|
||||||
: translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model');
|
: translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model');
|
||||||
@@ -325,10 +400,23 @@ function renderToolbar(record) {
|
|||||||
'Track and manage every version of this model in one place.'
|
'Track and manage every version of this model in one place.'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const displayMode = toolbarState.displayMode || DISPLAY_FILTER_MODES.ANY;
|
||||||
|
const toggleLabel = getToggleLabelText();
|
||||||
|
const toggleState = getToggleStateText(displayMode);
|
||||||
|
const toggleTooltip = getToggleTooltipText(displayMode);
|
||||||
|
const filterActive = toolbarState.isFilteringActive ? 'true' : 'false';
|
||||||
|
const screenReaderText = [toggleLabel, toggleState].filter(Boolean).join(': ');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<header class="versions-toolbar">
|
<header class="versions-toolbar">
|
||||||
<div class="versions-toolbar-info">
|
<div class="versions-toolbar-info">
|
||||||
<h3>${translate('modals.model.versions.heading', {}, 'Model versions')}</h3>
|
<div class="versions-toolbar-info-heading">
|
||||||
|
<h3>${translate('modals.model.versions.heading', {}, 'Model versions')}</h3>
|
||||||
|
<button class="versions-filter-toggle" data-versions-action="toggle-version-display-mode" type="button" title="${escapeHtml(toggleTooltip)}" aria-label="${escapeHtml(toggleTooltip)}" data-filter-active="${filterActive}" aria-pressed="${filterActive}">
|
||||||
|
<i class="fas fa-th-list" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">${escapeHtml(screenReaderText)}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p>${escapeHtml(infoText)}</p>
|
<p>${escapeHtml(infoText)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="versions-toolbar-actions">
|
<div class="versions-toolbar-actions">
|
||||||
@@ -353,6 +441,20 @@ function renderEmptyState(container) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFilteredEmptyState(baseModelLabel) {
|
||||||
|
const message = translate(
|
||||||
|
'modals.model.versions.filters.empty',
|
||||||
|
{ baseModel: baseModelLabel },
|
||||||
|
'No versions match the current base model filter.'
|
||||||
|
);
|
||||||
|
return `
|
||||||
|
<div class="versions-empty versions-empty-filter">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<p>${escapeHtml(message)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderErrorState(container, message) {
|
function renderErrorState(container, message) {
|
||||||
const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.');
|
const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@@ -391,6 +493,8 @@ export function initVersionsTab({
|
|||||||
record: null,
|
record: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let displayMode = getDefaultDisplayMode();
|
||||||
|
|
||||||
let apiClient;
|
let apiClient;
|
||||||
|
|
||||||
function ensureClient() {
|
function ensureClient() {
|
||||||
@@ -414,55 +518,92 @@ export function initVersionsTab({
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(record) {
|
function render(record) {
|
||||||
controller.record = record;
|
controller.record = record;
|
||||||
controller.hasLoaded = true;
|
controller.hasLoaded = true;
|
||||||
|
|
||||||
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
||||||
renderEmptyState(container);
|
renderEmptyState(container);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
|
||||||
let dividerInserted = false;
|
|
||||||
|
|
||||||
const sortedVersions = [...record.versions].sort(
|
|
||||||
(a, b) => Number(b.versionId) - Number(a.versionId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const rowsMarkup = sortedVersions
|
|
||||||
.map(version => {
|
|
||||||
const isNewer =
|
|
||||||
typeof latestLibraryVersionId === 'number' &&
|
|
||||||
version.versionId > latestLibraryVersionId;
|
|
||||||
let markup = '';
|
|
||||||
if (
|
|
||||||
!dividerInserted &&
|
|
||||||
typeof latestLibraryVersionId === 'number' &&
|
|
||||||
!isNewer
|
|
||||||
) {
|
|
||||||
dividerInserted = true;
|
|
||||||
markup += '<div class="version-divider" role="presentation"></div>';
|
|
||||||
}
|
|
||||||
markup += renderRow(version, {
|
|
||||||
latestLibraryVersionId,
|
|
||||||
currentVersionId: normalizedCurrentVersionId,
|
|
||||||
modelId: record?.modelId ?? modelId,
|
|
||||||
});
|
|
||||||
return markup;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
${renderToolbar(record)}
|
|
||||||
<div class="versions-list">
|
|
||||||
${rowsMarkup}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setupMediaHoverInteractions(container);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
||||||
|
const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } =
|
||||||
|
getCurrentVersionBaseModel(record, normalizedCurrentVersionId);
|
||||||
|
const isFilteringActive =
|
||||||
|
displayMode === DISPLAY_FILTER_MODES.SAME_BASE &&
|
||||||
|
Boolean(currentBaseModelNormalized);
|
||||||
|
|
||||||
|
const sortedVersions = [...record.versions].sort(
|
||||||
|
(a, b) => Number(b.versionId) - Number(a.versionId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredVersions = sortedVersions.filter(version => {
|
||||||
|
if (!isFilteringActive) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dividerThresholdVersionId = (() => {
|
||||||
|
if (!isFilteringActive) {
|
||||||
|
return latestLibraryVersionId;
|
||||||
|
}
|
||||||
|
const baseLocalVersionIds = record.versions
|
||||||
|
.filter(
|
||||||
|
version =>
|
||||||
|
version.isInLibrary &&
|
||||||
|
normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized &&
|
||||||
|
typeof version.versionId === 'number'
|
||||||
|
)
|
||||||
|
.map(version => version.versionId);
|
||||||
|
if (!baseLocalVersionIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.max(...baseLocalVersionIds);
|
||||||
|
})();
|
||||||
|
|
||||||
|
let dividerInserted = false;
|
||||||
|
|
||||||
|
const rowsMarkup = filteredVersions
|
||||||
|
.map(version => {
|
||||||
|
const isNewer =
|
||||||
|
typeof latestLibraryVersionId === 'number' &&
|
||||||
|
version.versionId > latestLibraryVersionId;
|
||||||
|
let markup = '';
|
||||||
|
if (
|
||||||
|
!dividerInserted &&
|
||||||
|
typeof dividerThresholdVersionId === 'number' &&
|
||||||
|
!(version.versionId > dividerThresholdVersionId)
|
||||||
|
) {
|
||||||
|
dividerInserted = true;
|
||||||
|
markup += '<div class="version-divider" role="presentation"></div>';
|
||||||
|
}
|
||||||
|
markup += renderRow(version, {
|
||||||
|
latestLibraryVersionId,
|
||||||
|
currentVersionId: normalizedCurrentVersionId,
|
||||||
|
modelId: record?.modelId ?? modelId,
|
||||||
|
});
|
||||||
|
return markup;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
const listContent =
|
||||||
|
rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
${renderToolbar(record, {
|
||||||
|
displayMode,
|
||||||
|
isFilteringActive,
|
||||||
|
})}
|
||||||
|
<div class="versions-list">
|
||||||
|
${listContent}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setupMediaHoverInteractions(container);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
|
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
|
||||||
if (controller.isLoading) {
|
if (controller.isLoading) {
|
||||||
return;
|
return;
|
||||||
@@ -531,6 +672,17 @@ export function initVersionsTab({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleToggleVersionDisplayMode() {
|
||||||
|
displayMode =
|
||||||
|
displayMode === DISPLAY_FILTER_MODES.SAME_BASE
|
||||||
|
? DISPLAY_FILTER_MODES.ANY
|
||||||
|
: DISPLAY_FILTER_MODES.SAME_BASE;
|
||||||
|
if (!controller.record) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render(controller.record);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleToggleVersionIgnore(button, versionId) {
|
async function handleToggleVersionIgnore(button, versionId) {
|
||||||
if (!controller.record) {
|
if (!controller.record) {
|
||||||
return;
|
return;
|
||||||
@@ -799,9 +951,17 @@ export function initVersionsTab({
|
|||||||
const toolbarAction = event.target.closest('[data-versions-action]');
|
const toolbarAction = event.target.closest('[data-versions-action]');
|
||||||
if (toolbarAction) {
|
if (toolbarAction) {
|
||||||
const action = toolbarAction.dataset.versionsAction;
|
const action = toolbarAction.dataset.versionsAction;
|
||||||
if (action === 'toggle-model-ignore') {
|
switch (action) {
|
||||||
event.preventDefault();
|
case 'toggle-model-ignore':
|
||||||
await handleToggleModelIgnore(toolbarAction);
|
event.preventDefault();
|
||||||
|
await handleToggleModelIgnore(toolbarAction);
|
||||||
|
break;
|
||||||
|
case 'toggle-version-display-mode':
|
||||||
|
event.preventDefault();
|
||||||
|
handleToggleVersionDisplayMode();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ vi.mock(UI_HELPERS_MODULE, () => ({
|
|||||||
showToast: vi.fn(),
|
showToast: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const stateMock = { global: { settings: { autoplay_on_hover: false } } };
|
const stateMock = {
|
||||||
|
global: {
|
||||||
|
settings: {
|
||||||
|
autoplay_on_hover: false,
|
||||||
|
update_flag_strategy: 'any',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
vi.mock(STATE_MODULE, () => ({
|
vi.mock(STATE_MODULE, () => ({
|
||||||
state: stateMock,
|
state: stateMock,
|
||||||
}));
|
}));
|
||||||
@@ -59,6 +66,7 @@ describe('ModelVersionsTab media rendering', () => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
stateMock.global.settings.autoplay_on_hover = false;
|
stateMock.global.settings.autoplay_on_hover = false;
|
||||||
|
stateMock.global.settings.update_flag_strategy = 'any';
|
||||||
({ getModelApiClient } = await import(API_FACTORY_MODULE));
|
({ getModelApiClient } = await import(API_FACTORY_MODULE));
|
||||||
fetchModelUpdateVersions = vi.fn();
|
fetchModelUpdateVersions = vi.fn();
|
||||||
getModelApiClient.mockReturnValue({
|
getModelApiClient.mockReturnValue({
|
||||||
@@ -146,4 +154,133 @@ describe('ModelVersionsTab media rendering', () => {
|
|||||||
expect(imageElement?.getAttribute('src')).toBe(previewUrl);
|
expect(imageElement?.getAttribute('src')).toBe(previewUrl);
|
||||||
expect(document.querySelector('.version-media video')).toBeFalsy();
|
expect(document.querySelector('.version-media video')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows a stable label with a short state indicator', async () => {
|
||||||
|
stateMock.global.settings.update_flag_strategy = 'any';
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
record: {
|
||||||
|
shouldIgnore: false,
|
||||||
|
inLibraryVersionIds: [5],
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
versionId: 5,
|
||||||
|
name: 'base',
|
||||||
|
baseModel: 'SDXL',
|
||||||
|
previewUrl: '/api/lm/previews/v-base.png',
|
||||||
|
sizeBytes: 1024,
|
||||||
|
isInLibrary: true,
|
||||||
|
shouldIgnore: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||||
|
const controller = initVersionsTab({
|
||||||
|
modalId: 'model-versions-modal',
|
||||||
|
modelType: 'loras',
|
||||||
|
modelId: 321,
|
||||||
|
currentVersionId: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
const toggleText = document.querySelector('.versions-filter-toggle .sr-only');
|
||||||
|
expect(toggleText?.textContent?.trim()).toBe('Base filter: All versions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters versions to the current base model when strategy is same_base', async () => {
|
||||||
|
stateMock.global.settings.update_flag_strategy = 'same_base';
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
record: {
|
||||||
|
shouldIgnore: false,
|
||||||
|
inLibraryVersionIds: [10],
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
versionId: 10,
|
||||||
|
name: 'v1.0',
|
||||||
|
baseModel: 'SDXL',
|
||||||
|
previewUrl: '/api/lm/previews/v1.png',
|
||||||
|
sizeBytes: 1024,
|
||||||
|
isInLibrary: true,
|
||||||
|
shouldIgnore: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
versionId: 11,
|
||||||
|
name: 'v1.1',
|
||||||
|
baseModel: 'Realistic',
|
||||||
|
previewUrl: '/api/lm/previews/v1-1.png',
|
||||||
|
sizeBytes: 2048,
|
||||||
|
isInLibrary: false,
|
||||||
|
shouldIgnore: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||||
|
const controller = initVersionsTab({
|
||||||
|
modalId: 'model-versions-modal',
|
||||||
|
modelType: 'loras',
|
||||||
|
modelId: 789,
|
||||||
|
currentVersionId: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(document.querySelectorAll('.model-version-row').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggle button can switch to display all versions', async () => {
|
||||||
|
stateMock.global.settings.update_flag_strategy = 'same_base';
|
||||||
|
fetchModelUpdateVersions.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
record: {
|
||||||
|
shouldIgnore: false,
|
||||||
|
inLibraryVersionIds: [10],
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
versionId: 10,
|
||||||
|
name: 'v1.0',
|
||||||
|
baseModel: 'SDXL',
|
||||||
|
previewUrl: '/api/lm/previews/v1.png',
|
||||||
|
sizeBytes: 1024,
|
||||||
|
isInLibrary: true,
|
||||||
|
shouldIgnore: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
versionId: 11,
|
||||||
|
name: 'v1.1',
|
||||||
|
baseModel: 'Realistic',
|
||||||
|
previewUrl: '/api/lm/previews/v1-1.png',
|
||||||
|
sizeBytes: 2048,
|
||||||
|
isInLibrary: false,
|
||||||
|
shouldIgnore: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||||
|
const controller = initVersionsTab({
|
||||||
|
modalId: 'model-versions-modal',
|
||||||
|
modelType: 'loras',
|
||||||
|
modelId: 987,
|
||||||
|
currentVersionId: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.load();
|
||||||
|
|
||||||
|
expect(document.querySelectorAll('.model-version-row').length).toBe(1);
|
||||||
|
const toggleButton = document.querySelector('[data-versions-action="toggle-version-display-mode"]');
|
||||||
|
expect(toggleButton).toBeTruthy();
|
||||||
|
const toggleTextBefore = document.querySelector('.versions-filter-toggle .sr-only');
|
||||||
|
expect(toggleTextBefore?.textContent?.trim()).toContain('Same base');
|
||||||
|
toggleButton?.click();
|
||||||
|
expect(document.querySelectorAll('.model-version-row').length).toBe(2);
|
||||||
|
const toggleTextAfter = document.querySelector('.versions-filter-toggle .sr-only');
|
||||||
|
expect(toggleTextAfter?.textContent?.trim()).toContain('All versions');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user