Add version sorting for plugins

Enhance plugin dialog with semver-aware sorting
by version, enabling users to sort plugins based
on version comparisons.
This commit is contained in:
Andrew
2026-07-06 13:01:21 +08:00
parent 714fe54f77
commit a11e442f1d
5 changed files with 52 additions and 2 deletions

View File

@@ -47,7 +47,8 @@
<span>Activate</span> <span>Activate</span>
<span class="sort-th" data-sort-field="name" role="button" tabindex="0" <span class="sort-th" data-sort-field="name" role="button" tabindex="0"
title="Sort by name">Name<span class="sort-tri" aria-hidden="true"></span></span> title="Sort by name">Name<span class="sort-tri" aria-hidden="true"></span></span>
<span>Plugin Version</span> <span class="sort-th" data-sort-field="version" role="button" tabindex="0"
title="Sort by version">Plugin Version<span class="sort-tri" aria-hidden="true"></span></span>
<span class="sort-th" data-sort-field="source" role="button" tabindex="0" <span class="sort-th" data-sort-field="source" role="button" tabindex="0"
title="Sort by source">Source<span class="sort-tri" aria-hidden="true"></span></span> title="Sort by source">Source<span class="sort-tri" aria-hidden="true"></span></span>
<span class="sort-th" data-sort-field="status" role="button" tabindex="0" <span class="sort-th" data-sort-field="status" role="button" tabindex="0"

View File

@@ -3,7 +3,7 @@
const DEFAULT_PLUGIN_SORT = { key: "none", order: "asc" }; const DEFAULT_PLUGIN_SORT = { key: "none", order: "asc" };
// note: SORT_FIELDS are the clickable columns. "none" is the baseline/cleared state, not a field - // note: SORT_FIELDS are the clickable columns. "none" is the baseline/cleared state, not a field -
// it is special-cased in NormalizePluginSort and produced by CyclePluginSort's third click. // it is special-cased in NormalizePluginSort and produced by CyclePluginSort's third click.
const SORT_FIELDS = new Set(["status", "name", "source"]); const SORT_FIELDS = new Set(["status", "name", "source", "version"]);
let pluginSort = { ...DEFAULT_PLUGIN_SORT }; let pluginSort = { ...DEFAULT_PLUGIN_SORT };
// why: C++ returns canonical sort state; guard stale or malformed values before reflecting them. // why: C++ returns canonical sort state; guard stale or malformed values before reflecting them.

View File

@@ -3,6 +3,8 @@
#include "PluginSource.hpp" #include "PluginSource.hpp"
#include "PluginStatus.hpp" #include "PluginStatus.hpp"
#include "libslic3r/Semver.hpp"
#include <algorithm> #include <algorithm>
#include <cctype> #include <cctype>
#include <string> #include <string>
@@ -16,6 +18,7 @@ namespace Slic3r::GUI
Status, Status,
Name, Name,
Source, Source,
Version,
// why: neutral "no column selected" state - clearing a header sort returns here and the // why: neutral "no column selected" state - clearing a header sort returns here and the
// list falls to compare_plugin_base_order only. Header UI reaches it via the asc/desc/clear cycle. // list falls to compare_plugin_base_order only. Header UI reaches it via the asc/desc/clear cycle.
None None
@@ -34,6 +37,7 @@ namespace Slic3r::GUI
case PluginSortKey::Status: return "status"; case PluginSortKey::Status: return "status";
case PluginSortKey::Name: return "name"; case PluginSortKey::Name: return "name";
case PluginSortKey::Source: return "source"; case PluginSortKey::Source: return "source";
case PluginSortKey::Version: return "version";
case PluginSortKey::None: return "none"; case PluginSortKey::None: return "none";
} }
@@ -53,6 +57,8 @@ namespace Slic3r::GUI
return PluginSortKey::Name; return PluginSortKey::Name;
if (sort_key == "source") if (sort_key == "source")
return PluginSortKey::Source; return PluginSortKey::Source;
if (sort_key == "version")
return PluginSortKey::Version;
if (sort_key == "none") if (sort_key == "none")
return PluginSortKey::None; return PluginSortKey::None;
return fallback; return fallback;
@@ -141,6 +147,23 @@ namespace Slic3r::GUI
return lhs.plugin_key.compare(rhs.plugin_key); return lhs.plugin_key.compare(rhs.plugin_key);
} }
// Compares two version strings returning -1 / 0 / +1. Uses Slic3r::Semver (the same parser the
// plugin catalog's update-available check uses); on unparseable input falls back to the natural
// compare so the order stays deterministic.
// e.g. "1.2.0" < "1.10.0" (numeric), "1.0.0-rc1" < "1.0.0" (semver prerelease rule).
inline int compare_plugin_version(const std::string& lhs, const std::string& rhs)
{
const auto lhs_semver = Semver::parse(lhs);
const auto rhs_semver = Semver::parse(rhs);
if (lhs_semver && rhs_semver)
{
if (*lhs_semver < *rhs_semver) return -1;
if (*rhs_semver < *lhs_semver) return 1;
return 0;
}
return compare_ascii_case_insensitive_natural(lhs, rhs);
}
// Compares two items by the chosen primary key, returning -1 / 0 / +1. Status and Source // Compares two items by the chosen primary key, returning -1 / 0 / +1. Status and Source
// rank by enum ordinal (the declared dialog priority); Name uses the natural compare above. // rank by enum ordinal (the declared dialog priority); Name uses the natural compare above.
// e.g. Status: an enabled item (lower ordinal) sorts before a disabled one. // e.g. Status: an enabled item (lower ordinal) sorts before a disabled one.
@@ -157,6 +180,8 @@ namespace Slic3r::GUI
return compare_ascii_case_insensitive_natural(lhs.display_name, rhs.display_name); return compare_ascii_case_insensitive_natural(lhs.display_name, rhs.display_name);
case PluginSortKey::Source: case PluginSortKey::Source:
return static_cast<int>(lhs.source) - static_cast<int>(rhs.source); return static_cast<int>(lhs.source) - static_cast<int>(rhs.source);
case PluginSortKey::Version:
return compare_plugin_version(lhs.sort_version, rhs.sort_version);
case PluginSortKey::None: case PluginSortKey::None:
// why: no primary key - every pair ties here so sort_plugin_items_for_dialog falls // why: no primary key - every pair ties here so sort_plugin_items_for_dialog falls
// straight to the ascending base order (direction is irrelevant for the baseline). // straight to the ascending base order (direction is irrelevant for the baseline).

View File

@@ -89,6 +89,7 @@ struct PluginDialogItem
std::string version; std::string version;
std::string installed_version; std::string installed_version;
std::string latest_version; std::string latest_version;
std::string sort_version; // Version shown in the row (installed if installed, else latest); used by the Version sort.
std::string type_label; std::string type_label;
std::string type_key; std::string type_key;
std::string sharing_token; std::string sharing_token;
@@ -319,6 +320,9 @@ PluginDialogItem build_plugin_dialog_item(const PluginDescriptor& descriptor)
(descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version) : (descriptor.installed_version.empty() ? descriptor.version : descriptor.installed_version) :
std::string{}; std::string{};
item.latest_version = descriptor.latest_available_version(); item.latest_version = descriptor.latest_available_version();
// why: sort by the same version the row displays (GetDisplayVersion in index.js) - installed when
// installed, otherwise latest - so the Version sort matches what the user sees.
item.sort_version = item.installed_version.empty() ? item.latest_version : item.installed_version;
item.type_label = descriptor.type_label(); item.type_label = descriptor.type_label();
item.type_key = plugin_capability_type_to_string(descriptor.primary_capability_type()); item.type_key = plugin_capability_type_to_string(descriptor.primary_capability_type());
// "types" is the display-only compatibility list. Cloud plugins show the raw labels the // "types" is the display-only compatibility list. Cloud plugins show the raw labels the

View File

@@ -23,6 +23,7 @@ struct SortFixtureItem
PluginStatus status; PluginStatus status;
std::string type_key; std::string type_key;
std::string display_name; std::string display_name;
std::string sort_version;
}; };
std::vector<std::string> keys(const std::vector<SortFixtureItem>& items) std::vector<std::string> keys(const std::vector<SortFixtureItem>& items)
@@ -90,6 +91,25 @@ TEST_CASE("plugin dialog source sort uses enum priority", "[plugin][sort]")
CHECK(keys(items) == desc_expected); CHECK(keys(items) == desc_expected);
} }
TEST_CASE("plugin dialog version sort is semver-aware with base-order ties", "[plugin][sort]")
{
std::vector<SortFixtureItem> items = {
{"v_1_2_0", PluginSource::Local, PluginStatus::Activated, "script", "B", "1.2.0"},
{"v_1_10_0", PluginSource::Local, PluginStatus::Activated, "script", "A", "1.10.0"},
{"v_0_9_3", PluginSource::Local, PluginStatus::Activated, "script", "C", "0.9.3"},
};
sort_plugin_items_for_dialog(items, PluginSortKey::Version, PluginSortOrder::Asc);
// why: semver numeric compare - 1.10.0 > 1.2.0 (not lexical "1.10" < "1.2"), so ascending is
// 0.9.3 < 1.2.0 < 1.10.0.
const std::vector<std::string> asc_expected = {"v_0_9_3", "v_1_2_0", "v_1_10_0"};
CHECK(keys(items) == asc_expected);
sort_plugin_items_for_dialog(items, PluginSortKey::Version, PluginSortOrder::Desc);
const std::vector<std::string> desc_expected = {"v_1_10_0", "v_1_2_0", "v_0_9_3"};
CHECK(keys(items) == desc_expected);
}
TEST_CASE("plugin dialog name sort is case-insensitive and numeric-aware", "[plugin][sort]") TEST_CASE("plugin dialog name sort is case-insensitive and numeric-aware", "[plugin][sort]")
{ {
std::vector<SortFixtureItem> items = { std::vector<SortFixtureItem> items = {