feat: Update recipes page with default descending date sort, refactor state properties for search/filters, and add new localization strings.

This commit is contained in:
Will Miao
2025-12-23 11:57:25 +08:00
parent 502c29c6bd
commit b044b329fc
16 changed files with 295 additions and 97 deletions

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "Bitte wählen Sie ein LoRA-Stammverzeichnis aus" "selectLoraRoot": "Bitte wählen Sie ein LoRA-Stammverzeichnis aus"
} }
}, },
"sort": {
"title": "Rezepte sortieren nach...",
"name": "Name",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "Datum",
"dateDesc": "Neueste",
"dateAsc": "Älteste",
"lorasCount": "LoRA-Anzahl",
"lorasCountDesc": "Meiste",
"lorasCountAsc": "Wenigste"
},
"refresh": { "refresh": {
"title": "Rezeptliste aktualisieren" "title": "Rezeptliste aktualisieren"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "Please select a LoRA root directory" "selectLoraRoot": "Please select a LoRA root directory"
} }
}, },
"sort": {
"title": "Sort recipes by...",
"name": "Name",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "Date",
"dateDesc": "Newest",
"dateAsc": "Oldest",
"lorasCount": "LoRA Count",
"lorasCountDesc": "Most",
"lorasCountAsc": "Least"
},
"refresh": { "refresh": {
"title": "Refresh recipe list" "title": "Refresh recipe list"
}, },

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "Por favor selecciona un directorio raíz de LoRA" "selectLoraRoot": "Por favor selecciona un directorio raíz de LoRA"
} }
}, },
"sort": {
"title": "Ordenar recetas por...",
"name": "Nombre",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "Fecha",
"dateDesc": "Más reciente",
"dateAsc": "Más antiguo",
"lorasCount": "Cant. de LoRAs",
"lorasCountDesc": "Más",
"lorasCountAsc": "Menos"
},
"refresh": { "refresh": {
"title": "Actualizar lista de recetas" "title": "Actualizar lista de recetas"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "Veuillez sélectionner un répertoire racine LoRA" "selectLoraRoot": "Veuillez sélectionner un répertoire racine LoRA"
} }
}, },
"sort": {
"title": "Trier les recettes par...",
"name": "Nom",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "Date",
"dateDesc": "Plus récent",
"dateAsc": "Plus ancien",
"lorasCount": "Nombre de LoRAs",
"lorasCountDesc": "Plus",
"lorasCountAsc": "Moins"
},
"refresh": { "refresh": {
"title": "Actualiser la liste des recipes" "title": "Actualiser la liste des recipes"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "אנא בחר ספריית שורש של LoRA" "selectLoraRoot": "אנא בחר ספריית שורש של LoRA"
} }
}, },
"sort": {
"title": "מיון מתכונים לפי...",
"name": "שם",
"nameAsc": "א - ת",
"nameDesc": "ת - א",
"date": "תאריך",
"dateDesc": "הכי חדש",
"dateAsc": "הכי ישן",
"lorasCount": "מספר LoRAs",
"lorasCountDesc": "הכי הרבה",
"lorasCountAsc": "הכי פחות"
},
"refresh": { "refresh": {
"title": "רענן רשימת מתכונים" "title": "רענן רשימת מתכונים"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "LoRAルートディレクトリを選択してください" "selectLoraRoot": "LoRAルートディレクトリを選択してください"
} }
}, },
"sort": {
"title": "レシピの並び替え...",
"name": "名前",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "日付",
"dateDesc": "新しい順",
"dateAsc": "古い順",
"lorasCount": "LoRA数",
"lorasCountDesc": "多い順",
"lorasCountAsc": "少ない順"
},
"refresh": { "refresh": {
"title": "レシピリストを更新" "title": "レシピリストを更新"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "LoRA 루트 디렉토리를 선택해주세요" "selectLoraRoot": "LoRA 루트 디렉토리를 선택해주세요"
} }
}, },
"sort": {
"title": "레시피 정렬...",
"name": "이름",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "날짜",
"dateDesc": "최신순",
"dateAsc": "오래된순",
"lorasCount": "LoRA 수",
"lorasCountDesc": "많은순",
"lorasCountAsc": "적은순"
},
"refresh": { "refresh": {
"title": "레시피 목록 새로고침" "title": "레시피 목록 새로고침"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "Пожалуйста, выберите корневую папку LoRA" "selectLoraRoot": "Пожалуйста, выберите корневую папку LoRA"
} }
}, },
"sort": {
"title": "Сортировка рецептов...",
"name": "Имя",
"nameAsc": "А - Я",
"nameDesc": "Я - А",
"date": "Дата",
"dateDesc": "Сначала новые",
"dateAsc": "Сначала старые",
"lorasCount": "Кол-во LoRA",
"lorasCountDesc": "Больше всего",
"lorasCountAsc": "Меньше всего"
},
"refresh": { "refresh": {
"title": "Обновить список рецептов" "title": "Обновить список рецептов"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "请选择 LoRA 根目录" "selectLoraRoot": "请选择 LoRA 根目录"
} }
}, },
"sort": {
"title": "配方排序...",
"name": "名称",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "时间",
"dateDesc": "最新",
"dateAsc": "最早",
"lorasCount": "LoRA 数量",
"lorasCountDesc": "最多",
"lorasCountAsc": "最少"
},
"refresh": { "refresh": {
"title": "刷新配方列表" "title": "刷新配方列表"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "浏览器插件教程" "learnMore": "浏览器插件教程"
} }
} }
} }

View File

@@ -589,6 +589,18 @@
"selectLoraRoot": "請選擇 LoRA 根目錄" "selectLoraRoot": "請選擇 LoRA 根目錄"
} }
}, },
"sort": {
"title": "配方排序...",
"name": "名稱",
"nameAsc": "A - Z",
"nameDesc": "Z - A",
"date": "時間",
"dateDesc": "最新",
"dateAsc": "最舊",
"lorasCount": "LoRA 數量",
"lorasCountDesc": "最多",
"lorasCountAsc": "最少"
},
"refresh": { "refresh": {
"title": "重新整理配方列表" "title": "重新整理配方列表"
}, },
@@ -1485,4 +1497,4 @@
"learnMore": "LM Civitai Extension Tutorial" "learnMore": "LM Civitai Extension Tutorial"
} }
} }
} }

View File

@@ -1024,7 +1024,14 @@ class RecipeScanner:
cache = await self.get_cached_data() cache = await self.get_cached_data()
# Get base dataset # Get base dataset
filtered_data = cache.sorted_by_date if sort_by == 'date' else cache.sorted_by_name sort_field = sort_by.split(':')[0] if ':' in sort_by else sort_by
if sort_field == 'date':
filtered_data = list(cache.sorted_by_date)
elif sort_field == 'name':
filtered_data = list(cache.sorted_by_name)
else:
filtered_data = list(cache.raw_data)
# Apply SFW filtering if enabled # Apply SFW filtering if enabled
from .settings_manager import get_settings_manager from .settings_manager import get_settings_manager
@@ -1166,6 +1173,20 @@ class RecipeScanner:
if not any(tag in exclude_tags for tag in (item.get('tags', []) or [])) if not any(tag in exclude_tags for tag in (item.get('tags', []) or []))
] ]
# Apply sorting if not already handled by pre-sorted cache
if ':' in sort_by or sort_field == 'loras_count':
field, order = (sort_by.split(':') + ['desc'])[:2]
reverse = order.lower() == 'desc'
if field == 'name':
filtered_data = natsorted(filtered_data, key=lambda x: x.get('title', '').lower(), reverse=reverse)
elif field == 'date':
# Use modified if available, falling back to created_date
filtered_data.sort(key=lambda x: (x.get('modified', x.get('created_date', 0)), x.get('file_path', '')), reverse=reverse)
elif field == 'loras_count':
filtered_data.sort(key=lambda x: len(x.get('loras', [])), reverse=reverse)
# Calculate pagination # Calculate pagination
total_items = len(filtered_data) total_items = len(filtered_data)
start_idx = (page - 1) * page_size start_idx = (page - 1) * page_size

View File

@@ -216,6 +216,7 @@ class RecipeManager {
// Sort select // Sort select
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
if (sortSelect) { if (sortSelect) {
sortSelect.value = this.pageState.sortBy || 'date:desc';
sortSelect.addEventListener('change', () => { sortSelect.addEventListener('change', () => {
this.pageState.sortBy = sortSelect.value; this.pageState.sortBy = sortSelect.value;
refreshVirtualScroll(); refreshVirtualScroll();

View File

@@ -58,7 +58,7 @@ export const state = {
loadingManager: null, loadingManager: null,
observer: null, observer: null,
}, },
// Page-specific states // Page-specific states
pages: { pages: {
[MODEL_TYPES.LORA]: { [MODEL_TYPES.LORA]: {
@@ -69,20 +69,20 @@ export const state = {
activeFolder: getStorageItem(`${MODEL_TYPES.LORA}_activeFolder`), activeFolder: getStorageItem(`${MODEL_TYPES.LORA}_activeFolder`),
activeLetterFilter: null, activeLetterFilter: null,
previewVersions: loraPreviewVersions, previewVersions: loraPreviewVersions,
searchManager: null, searchManager: null,
searchOptions: { searchOptions: {
filename: true, filename: true,
modelname: true, modelname: true,
tags: false, tags: false,
creator: false, creator: false,
recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true), recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true),
}, },
filters: { filters: {
baseModel: [], baseModel: [],
tags: {}, tags: {},
license: {}, license: {},
modelTypes: [] modelTypes: []
}, },
bulkMode: false, bulkMode: false,
selectedLoras: new Set(), selectedLoras: new Set(),
loraMetadataCache: new Map(), loraMetadataCache: new Map(),
@@ -90,35 +90,35 @@ export const state = {
showUpdateAvailableOnly: false, showUpdateAvailableOnly: false,
duplicatesMode: false, duplicatesMode: false,
}, },
recipes: { recipes: {
currentPage: 1, currentPage: 1,
isLoading: false, isLoading: false,
hasMore: true, hasMore: true,
sortBy: 'date', sortBy: 'date:desc',
activeFolder: getStorageItem('recipes_activeFolder'), activeFolder: getStorageItem('recipes_activeFolder'),
searchManager: null, searchManager: null,
searchOptions: { searchOptions: {
title: true, title: true,
tags: true, tags: true,
loraName: true, loraName: true,
loraModel: true, loraModel: true,
recursive: getStorageItem('recipes_recursiveSearch', true), recursive: getStorageItem('recipes_recursiveSearch', true),
}, },
filters: { filters: {
baseModel: [], baseModel: [],
tags: {}, tags: {},
license: {}, license: {},
modelTypes: [], modelTypes: [],
search: '' search: ''
}, },
pageSize: 20, pageSize: 20,
showFavoritesOnly: false, showFavoritesOnly: false,
duplicatesMode: false, duplicatesMode: false,
bulkMode: false, bulkMode: false,
selectedModels: new Set(), selectedModels: new Set(),
}, },
[MODEL_TYPES.CHECKPOINT]: { [MODEL_TYPES.CHECKPOINT]: {
currentPage: 1, currentPage: 1,
isLoading: false, isLoading: false,
@@ -126,19 +126,19 @@ export const state = {
sortBy: 'name', sortBy: 'name',
activeFolder: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_activeFolder`), activeFolder: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_activeFolder`),
previewVersions: checkpointPreviewVersions, previewVersions: checkpointPreviewVersions,
searchManager: null, searchManager: null,
searchOptions: { searchOptions: {
filename: true, filename: true,
modelname: true, modelname: true,
creator: false, creator: false,
recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true), recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true),
}, },
filters: { filters: {
baseModel: [], baseModel: [],
tags: {}, tags: {},
license: {}, license: {},
modelTypes: [] modelTypes: []
}, },
modelType: 'checkpoint', // 'checkpoint' or 'diffusion_model' modelType: 'checkpoint', // 'checkpoint' or 'diffusion_model'
bulkMode: false, bulkMode: false,
selectedModels: new Set(), selectedModels: new Set(),
@@ -147,7 +147,7 @@ export const state = {
showUpdateAvailableOnly: false, showUpdateAvailableOnly: false,
duplicatesMode: false, duplicatesMode: false,
}, },
[MODEL_TYPES.EMBEDDING]: { [MODEL_TYPES.EMBEDDING]: {
currentPage: 1, currentPage: 1,
isLoading: false, isLoading: false,
@@ -156,20 +156,20 @@ export const state = {
activeFolder: getStorageItem(`${MODEL_TYPES.EMBEDDING}_activeFolder`), activeFolder: getStorageItem(`${MODEL_TYPES.EMBEDDING}_activeFolder`),
activeLetterFilter: null, activeLetterFilter: null,
previewVersions: embeddingPreviewVersions, previewVersions: embeddingPreviewVersions,
searchManager: null, searchManager: null,
searchOptions: { searchOptions: {
filename: true, filename: true,
modelname: true, modelname: true,
tags: false, tags: false,
creator: false, creator: false,
recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true), recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true),
}, },
filters: { filters: {
baseModel: [], baseModel: [],
tags: {}, tags: {},
license: {}, license: {},
modelTypes: [] modelTypes: []
}, },
bulkMode: false, bulkMode: false,
selectedModels: new Set(), selectedModels: new Set(),
metadataCache: new Map(), metadataCache: new Map(),
@@ -178,45 +178,45 @@ export const state = {
duplicatesMode: false, duplicatesMode: false,
} }
}, },
// Current active page - use MODEL_TYPES constants // Current active page - use MODEL_TYPES constants
currentPageType: MODEL_TYPES.LORA, currentPageType: MODEL_TYPES.LORA,
// Backward compatibility - proxy properties // Backward compatibility - proxy properties
get currentPage() { return this.pages[this.currentPageType].currentPage; }, get currentPage() { return this.pages[this.currentPageType].currentPage; },
set currentPage(value) { this.pages[this.currentPageType].currentPage = value; }, set currentPage(value) { this.pages[this.currentPageType].currentPage = value; },
get isLoading() { return this.pages[this.currentPageType].isLoading; }, get isLoading() { return this.pages[this.currentPageType].isLoading; },
set isLoading(value) { this.pages[this.currentPageType].isLoading = value; }, set isLoading(value) { this.pages[this.currentPageType].isLoading = value; },
get hasMore() { return this.pages[this.currentPageType].hasMore; }, get hasMore() { return this.pages[this.currentPageType].hasMore; },
set hasMore(value) { this.pages[this.currentPageType].hasMore = value; }, set hasMore(value) { this.pages[this.currentPageType].hasMore = value; },
get sortBy() { return this.pages[this.currentPageType].sortBy; }, get sortBy() { return this.pages[this.currentPageType].sortBy; },
set sortBy(value) { this.pages[this.currentPageType].sortBy = value; }, set sortBy(value) { this.pages[this.currentPageType].sortBy = value; },
get activeFolder() { return this.pages[this.currentPageType].activeFolder; }, get activeFolder() { return this.pages[this.currentPageType].activeFolder; },
set activeFolder(value) { this.pages[this.currentPageType].activeFolder = value; }, set activeFolder(value) { this.pages[this.currentPageType].activeFolder = value; },
get loadingManager() { return this.global.loadingManager; }, get loadingManager() { return this.global.loadingManager; },
set loadingManager(value) { this.global.loadingManager = value; }, set loadingManager(value) { this.global.loadingManager = value; },
get observer() { return this.global.observer; }, get observer() { return this.global.observer; },
set observer(value) { this.global.observer = value; }, set observer(value) { this.global.observer = value; },
get previewVersions() { return this.pages.loras.previewVersions; }, get previewVersions() { return this.pages.loras.previewVersions; },
set previewVersions(value) { this.pages.loras.previewVersions = value; }, set previewVersions(value) { this.pages.loras.previewVersions = value; },
get searchManager() { return this.pages[this.currentPageType].searchManager; }, get searchManager() { return this.pages[this.currentPageType].searchManager; },
set searchManager(value) { this.pages[this.currentPageType].searchManager = value; }, set searchManager(value) { this.pages[this.currentPageType].searchManager = value; },
get searchOptions() { return this.pages[this.currentPageType].searchOptions; }, get searchOptions() { return this.pages[this.currentPageType].searchOptions; },
set searchOptions(value) { this.pages[this.currentPageType].searchOptions = value; }, set searchOptions(value) { this.pages[this.currentPageType].searchOptions = value; },
get filters() { return this.pages[this.currentPageType].filters; }, get filters() { return this.pages[this.currentPageType].filters; },
set filters(value) { this.pages[this.currentPageType].filters = value; }, set filters(value) { this.pages[this.currentPageType].filters = value; },
get bulkMode() { get bulkMode() {
const currentType = this.currentPageType; const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) { if (currentType === MODEL_TYPES.LORA) {
return this.pages.loras.bulkMode; return this.pages.loras.bulkMode;
@@ -224,7 +224,7 @@ export const state = {
return this.pages[currentType].bulkMode; return this.pages[currentType].bulkMode;
} }
}, },
set bulkMode(value) { set bulkMode(value) {
const currentType = this.currentPageType; const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) { if (currentType === MODEL_TYPES.LORA) {
this.pages.loras.bulkMode = value; this.pages.loras.bulkMode = value;
@@ -232,11 +232,11 @@ export const state = {
this.pages[currentType].bulkMode = value; this.pages[currentType].bulkMode = value;
} }
}, },
get selectedLoras() { return this.pages.loras.selectedLoras; }, get selectedLoras() { return this.pages.loras.selectedLoras; },
set selectedLoras(value) { this.pages.loras.selectedLoras = value; }, set selectedLoras(value) { this.pages.loras.selectedLoras = value; },
get selectedModels() { get selectedModels() {
const currentType = this.currentPageType; const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) { if (currentType === MODEL_TYPES.LORA) {
return this.pages.loras.selectedLoras; return this.pages.loras.selectedLoras;
@@ -244,7 +244,7 @@ export const state = {
return this.pages[currentType].selectedModels; return this.pages[currentType].selectedModels;
} }
}, },
set selectedModels(value) { set selectedModels(value) {
const currentType = this.currentPageType; const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) { if (currentType === MODEL_TYPES.LORA) {
this.pages.loras.selectedLoras = value; this.pages.loras.selectedLoras = value;
@@ -252,10 +252,10 @@ export const state = {
this.pages[currentType].selectedModels = value; this.pages[currentType].selectedModels = value;
} }
}, },
get loraMetadataCache() { return this.pages.loras.loraMetadataCache; }, get loraMetadataCache() { return this.pages.loras.loraMetadataCache; },
set loraMetadataCache(value) { this.pages.loras.loraMetadataCache = value; }, set loraMetadataCache(value) { this.pages.loras.loraMetadataCache = value; },
get settings() { return this.global.settings; }, get settings() { return this.global.settings; },
set settings(value) { this.global.settings = value; } set settings(value) { this.global.settings = value; }
}; };

View File

@@ -46,6 +46,22 @@
<!-- Recipe controls --> <!-- Recipe controls -->
<div class="controls"> <div class="controls">
<div class="action-buttons"> <div class="action-buttons">
<div class="control-group">
<select id="sortSelect" title="{{ t('recipes.controls.sort.title') }}">
<optgroup label="{{ t('recipes.controls.sort.name') }}">
<option value="name:asc">{{ t('recipes.controls.sort.nameAsc') }}</option>
<option value="name:desc">{{ t('recipes.controls.sort.nameDesc') }}</option>
</optgroup>
<optgroup label="{{ t('recipes.controls.sort.date') }}">
<option value="date:desc">{{ t('recipes.controls.sort.dateDesc') }}</option>
<option value="date:asc">{{ t('recipes.controls.sort.dateAsc') }}</option>
</optgroup>
<optgroup label="{{ t('recipes.controls.sort.lorasCount') }}">
<option value="loras_count:desc">{{ t('recipes.controls.sort.lorasCountDesc') }}</option>
<option value="loras_count:asc">{{ t('recipes.controls.sort.lorasCountAsc') }}</option>
</optgroup>
</select>
</div>
<div title="{{ t('recipes.controls.refresh.title') }}" class="control-group"> <div title="{{ t('recipes.controls.refresh.title') }}" class="control-group">
<button onclick="recipeManager.refreshRecipes()"><i class="fas fa-sync"></i> {{ t('common.actions.refresh') <button onclick="recipeManager.refreshRecipes()"><i class="fas fa-sync"></i> {{ t('common.actions.refresh')
}}</button> }}</button>

View File

@@ -100,7 +100,7 @@ describe('RecipeManager', () => {
}; };
pageState = { pageState = {
sortBy: 'date', sortBy: 'date:desc',
searchOptions: undefined, searchOptions: undefined,
customFilter: undefined, customFilter: undefined,
duplicatesMode: false, duplicatesMode: false,
@@ -137,8 +137,8 @@ describe('RecipeManager', () => {
const sortSelectElement = document.createElement('select'); const sortSelectElement = document.createElement('select');
sortSelectElement.id = 'sortSelect'; sortSelectElement.id = 'sortSelect';
sortSelectElement.innerHTML = ` sortSelectElement.innerHTML = `
<option value="date">Date</option> <option value="date:desc">Newest</option>
<option value="name">Name</option> <option value="name:asc">Name A-Z</option>
`; `;
document.body.appendChild(sortSelectElement); document.body.appendChild(sortSelectElement);
@@ -183,10 +183,10 @@ describe('RecipeManager', () => {
expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(1); expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(1);
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
sortSelect.value = 'name'; sortSelect.value = 'name:asc';
sortSelect.dispatchEvent(new Event('change', { bubbles: true })); sortSelect.dispatchEvent(new Event('change', { bubbles: true }));
expect(pageState.sortBy).toBe('name'); expect(pageState.sortBy).toBe('name:asc');
expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(2); expect(refreshVirtualScrollMock).toHaveBeenCalledTimes(2);
expect(initializePageFeaturesMock).toHaveBeenCalledTimes(1); expect(initializePageFeaturesMock).toHaveBeenCalledTimes(1);
}); });

View File

@@ -601,3 +601,43 @@ async def test_get_paginated_data_filters_by_prompt(recipe_scanner):
page=1, page_size=10, search="forest", search_options={"prompt": False} page=1, page_size=10, search="forest", search_options={"prompt": False}
) )
assert len(result_disabled["items"]) == 0 assert len(result_disabled["items"]) == 0
@pytest.mark.asyncio
async def test_get_paginated_data_sorting(recipe_scanner):
scanner, _ = recipe_scanner
# Add test recipes
# Recipe A: Name "Alpha", Date 10, LoRAs 2
await scanner.add_recipe({
"id": "A", "title": "Alpha", "created_date": 10.0,
"loras": [{}, {}], "file_path": "a.png"
})
# Recipe B: Name "Beta", Date 20, LoRAs 1
await scanner.add_recipe({
"id": "B", "title": "Beta", "created_date": 20.0,
"loras": [{}], "file_path": "b.png"
})
# Recipe C: Name "Gamma", Date 5, LoRAs 3
await scanner.add_recipe({
"id": "C", "title": "Gamma", "created_date": 5.0,
"loras": [{}, {}, {}], "file_path": "c.png"
})
await asyncio.sleep(0)
# Test Name DESC: Gamma, Beta, Alpha
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="name:desc")
assert [i["id"] for i in res["items"]] == ["C", "B", "A"]
# Test LoRA Count DESC: Gamma (3), Alpha (2), Beta (1)
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="loras_count:desc")
assert [i["id"] for i in res["items"]] == ["C", "A", "B"]
# Test LoRA Count ASC: Beta (1), Alpha (2), Gamma (3)
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="loras_count:asc")
assert [i["id"] for i in res["items"]] == ["B", "A", "C"]
# Test Date ASC: Gamma (5), Alpha (10), Beta (20)
res = await scanner.get_paginated_data(page=1, page_size=10, sort_by="date:asc")
assert [i["id"] for i in res["items"]] == ["C", "A", "B"]