mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-25 12:31:15 -03:00
feat(sort): enable versions_count sort in non-grouped mode
Sort by Most/Fewest versions first now works when Group by model is off. - Backend: group items by modelId (respecting version_grouping setting), count versions per group, sort groups by count, expand groups with versions sorted by version id descending - CSS: remove rule that hid the sort option in non-grouped mode - Tests: add 3 tests covering desc, asc, and same_base variants
This commit is contained in:
@@ -152,18 +152,51 @@ class BaseModelService(ABC):
|
||||
dedup_lost = len(sorted_data) - (len(dedup_map) + len(standalone))
|
||||
sorted_data = [entry[0] for entry in dedup_map.values()] + standalone
|
||||
|
||||
# Re-sort by version_count after dedup (only makes sense in group_by_model mode)
|
||||
is_group_by_active = kwargs.get("group_by_model") and civitai_model_id is None
|
||||
if sort_params.key == "versions_count" and is_group_by_active:
|
||||
# Re-sort by version_count (grouped: after dedup; non-grouped: group internally, sort, expand)
|
||||
if sort_params.key == "versions_count" and civitai_model_id is None:
|
||||
reverse = sort_params.order == "desc"
|
||||
sorted_data.sort(
|
||||
key=lambda x: (
|
||||
x.get("version_count", 0),
|
||||
(x.get("model_name") or x.get("file_name") or "").lower(),
|
||||
x.get("file_path", "").lower(),
|
||||
),
|
||||
reverse=reverse,
|
||||
)
|
||||
if kwargs.get("group_by_model"):
|
||||
# Grouped mode: items are already dedup'd with version_count attached
|
||||
sorted_data.sort(
|
||||
key=lambda x: (
|
||||
x.get("version_count", 0),
|
||||
(x.get("model_name") or x.get("file_name") or "").lower(),
|
||||
x.get("file_path", "").lower(),
|
||||
),
|
||||
reverse=reverse,
|
||||
)
|
||||
else:
|
||||
# Non-grouped mode: group internally, sort groups by count, expand
|
||||
# Respect the version_grouping setting (same logic as grouped dedup)
|
||||
ufs = self.settings.get("version_grouping", "same_base")
|
||||
group_by_base = ufs == "same_base"
|
||||
|
||||
model_groups: Dict[Any, List[Dict]] = {}
|
||||
ungrouped_standalone: List[Dict] = []
|
||||
for item in sorted_data:
|
||||
mid = self._extract_model_id(item)
|
||||
if mid is None:
|
||||
ungrouped_standalone.append(item)
|
||||
continue
|
||||
key = (mid, item.get("base_model") or "") if group_by_base else mid
|
||||
model_groups.setdefault(key, []).append(item)
|
||||
# Sort versions within each group by version id descending
|
||||
for items in model_groups.values():
|
||||
items.sort(
|
||||
key=lambda x: self._extract_version_id(x) or 0,
|
||||
reverse=True,
|
||||
)
|
||||
# Sort groups by version count
|
||||
sorted_groups = sorted(
|
||||
model_groups.values(),
|
||||
key=lambda items: len(items),
|
||||
reverse=reverse,
|
||||
)
|
||||
# Flatten: grouped items first, standalone items last
|
||||
sorted_data = []
|
||||
for items in sorted_groups:
|
||||
sorted_data.extend(items)
|
||||
sorted_data.extend(ungrouped_standalone)
|
||||
|
||||
t1 = time.perf_counter()
|
||||
if hash_filters:
|
||||
|
||||
@@ -60,7 +60,4 @@
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
/* Hide versions_count sort option when group-by-model is off */
|
||||
body:not(.group-by-model) .sort-option-versions-count {
|
||||
display: none;
|
||||
}
|
||||
/* ---------- reused from shared styles ---------- */
|
||||
|
||||
@@ -809,6 +809,136 @@ async def test_get_paginated_data_group_by_model_dedup():
|
||||
assert response_all["total"] == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_versions_count_non_grouped_desc():
|
||||
"""Non-grouped, versions_count:desc — groups by model, sorts by count desc,
|
||||
within-group by version id desc, then flattens."""
|
||||
items = [
|
||||
# modelId=1 has 3 versions
|
||||
{"model_name": "ModelA", "folder": "root", "civitai": {"modelId": 1, "id": 300}},
|
||||
{"model_name": "ModelA", "folder": "root", "civitai": {"modelId": 1, "id": 200}},
|
||||
{"model_name": "ModelA", "folder": "root", "civitai": {"modelId": 1, "id": 100}},
|
||||
# modelId=2 has 2 versions
|
||||
{"model_name": "ModelB", "folder": "root", "civitai": {"modelId": 2, "id": 99}},
|
||||
{"model_name": "ModelB", "folder": "root", "civitai": {"modelId": 2, "id": 50}},
|
||||
# modelId=3 has 1 version
|
||||
{"model_name": "ModelC", "folder": "root", "civitai": {"modelId": 3, "id": 1}},
|
||||
# standalone (no modelId)
|
||||
{"model_name": "Standalone", "folder": "root"},
|
||||
]
|
||||
repository = StubRepository(items)
|
||||
filter_set = PassThroughFilterSet()
|
||||
search_strategy = NoSearchStrategy()
|
||||
settings = StubSettings({})
|
||||
|
||||
service = DummyService(
|
||||
model_type="stub",
|
||||
scanner=object(),
|
||||
metadata_class=BaseModelMetadata,
|
||||
cache_repository=repository,
|
||||
filter_set=filter_set,
|
||||
search_strategy=search_strategy,
|
||||
settings_provider=settings,
|
||||
)
|
||||
|
||||
response = await service.get_paginated_data(
|
||||
page=1, page_size=10, sort_by="versions_count:desc",
|
||||
)
|
||||
|
||||
ids = [item["civitai"]["id"] for item in response["items"] if "civitai" in item and "id" in item["civitai"]]
|
||||
# modelId=1 (3 versions): id descending → 300, 200, 100
|
||||
# modelId=2 (2 versions): id descending → 99, 50
|
||||
# modelId=3 (1 version) → 1
|
||||
assert ids == [300, 200, 100, 99, 50, 1], f"Unexpected order: {ids}"
|
||||
assert response["total"] == 7
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_versions_count_non_grouped_asc():
|
||||
"""Non-grouped, versions_count:asc — groups by model, sorts by count asc,
|
||||
then flattens."""
|
||||
items = [
|
||||
# modelId=1 has 3 versions
|
||||
{"model_name": "ModelA", "folder": "root", "civitai": {"modelId": 1, "id": 300}},
|
||||
{"model_name": "ModelA", "folder": "root", "civitai": {"modelId": 1, "id": 200}},
|
||||
{"model_name": "ModelA", "folder": "root", "civitai": {"modelId": 1, "id": 100}},
|
||||
# modelId=2 has 2 versions
|
||||
{"model_name": "ModelB", "folder": "root", "civitai": {"modelId": 2, "id": 99}},
|
||||
{"model_name": "ModelB", "folder": "root", "civitai": {"modelId": 2, "id": 50}},
|
||||
# modelId=3 has 1 version
|
||||
{"model_name": "ModelC", "folder": "root", "civitai": {"modelId": 3, "id": 1}},
|
||||
# standalone (no modelId)
|
||||
{"model_name": "Standalone", "folder": "root"},
|
||||
]
|
||||
repository = StubRepository(items)
|
||||
filter_set = PassThroughFilterSet()
|
||||
search_strategy = NoSearchStrategy()
|
||||
settings = StubSettings({})
|
||||
|
||||
service = DummyService(
|
||||
model_type="stub",
|
||||
scanner=object(),
|
||||
metadata_class=BaseModelMetadata,
|
||||
cache_repository=repository,
|
||||
filter_set=filter_set,
|
||||
search_strategy=search_strategy,
|
||||
settings_provider=settings,
|
||||
)
|
||||
|
||||
response = await service.get_paginated_data(
|
||||
page=1, page_size=10, sort_by="versions_count:asc",
|
||||
)
|
||||
|
||||
ids = [item["civitai"]["id"] for item in response["items"] if "civitai" in item and "id" in item["civitai"]]
|
||||
# modelId=3 (1 version) → 1
|
||||
# modelId=2 (2 versions): id descending → 99, 50
|
||||
# modelId=1 (3 versions): id descending → 300, 200, 100
|
||||
assert ids == [1, 99, 50, 300, 200, 100], f"Unexpected order: {ids}"
|
||||
assert response["total"] == 7
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_versions_count_non_grouped_same_base():
|
||||
"""Non-grouped, versions_count with version_grouping=same_base —
|
||||
models with same modelId but different base_model are separate groups."""
|
||||
items = [
|
||||
# modelId=1, base_model="sd15" — 2 versions
|
||||
{"model_name": "ModelA", "folder": "root", "base_model": "sd15", "civitai": {"modelId": 1, "id": 200}},
|
||||
{"model_name": "ModelA", "folder": "root", "base_model": "sd15", "civitai": {"modelId": 1, "id": 100}},
|
||||
# modelId=1, base_model="sdxl" — 3 versions
|
||||
{"model_name": "ModelA", "folder": "root", "base_model": "sdxl", "civitai": {"modelId": 1, "id": 30}},
|
||||
{"model_name": "ModelA", "folder": "root", "base_model": "sdxl", "civitai": {"modelId": 1, "id": 20}},
|
||||
{"model_name": "ModelA", "folder": "root", "base_model": "sdxl", "civitai": {"modelId": 1, "id": 10}},
|
||||
# modelId=2, base_model="sd15" — 1 version
|
||||
{"model_name": "ModelB", "folder": "root", "base_model": "sd15", "civitai": {"modelId": 2, "id": 1}},
|
||||
]
|
||||
repository = StubRepository(items)
|
||||
filter_set = PassThroughFilterSet()
|
||||
search_strategy = NoSearchStrategy()
|
||||
settings = StubSettings({"version_grouping": "same_base"})
|
||||
|
||||
service = DummyService(
|
||||
model_type="stub",
|
||||
scanner=object(),
|
||||
metadata_class=BaseModelMetadata,
|
||||
cache_repository=repository,
|
||||
filter_set=filter_set,
|
||||
search_strategy=search_strategy,
|
||||
settings_provider=settings,
|
||||
)
|
||||
|
||||
response = await service.get_paginated_data(
|
||||
page=1, page_size=10, sort_by="versions_count:desc",
|
||||
)
|
||||
|
||||
ids = [item["civitai"]["id"] for item in response["items"] if "civitai" in item and "id" in item["civitai"]]
|
||||
# (1, "sdxl") — 3 versions: 30, 20, 10
|
||||
# (1, "sd15") — 2 versions: 200, 100
|
||||
# (2, "sd15") — 1 version: 1
|
||||
assert ids == [30, 20, 10, 200, 100, 1], f"Unexpected order: {ids}"
|
||||
assert response["total"] == 6
|
||||
|
||||
|
||||
async def test_get_paginated_data_filters_by_civitai_model_id():
|
||||
"""civitai_model_id filter returns only items matching the given modelId,
|
||||
and bypasses group_by_model dedup so all versions appear."""
|
||||
|
||||
Reference in New Issue
Block a user