Files
silo-server/internal/api/handlers/plugins_user_settings_test.go
1f2125b920 feat(plugins): group Apps sidebar by plugin manifest category (#366)
* feat(plugins): group Apps sidebar by plugin manifest category

Implements the plugin SDK's documented PluginManifest.category semantics
(silo-plugin-sdk proto/silo/plugin/v1/common.proto): a slash-delimited
path that groups plugins in the user-facing Apps section, e.g.
"Books/Audiobooks" lands under Apps -> Books. The field existed in the
manifest proto but silo-server never surfaced it.

Server: the user plugin-settings list/detail responses now include an
additive-only `category,omitempty` string sourced from the already-loaded
manifest via GetCategory(); no new parsing paths.

Web: PluginSettingsSummary gains `category?: string`, and AppSidebar
groups Apps entries by the FIRST segment of the category path (one level
of grouping for now; deeper segments intentionally ignored, documented
against the SDK contract). When fewer than 2 distinct categories exist
among the visible app links, today's flat list under the single "Apps"
header is kept; with 2+ categories, per-category sub-headers render via
the existing SidebarSectionHeader (labels hide in the collapsed sidebar
the same way other section headers do). Uncategorized plugins fall under
"Other", which always sorts last.

Tests: Go unit tests for the summary converter (category passthrough and
JSON omission when empty) and vitest coverage for the pure
groupAppNavLinks helper plus grouped/flat/collapsed sidebar rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(plugins): use generic category examples in comments and tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(web): simplify Apps sidebar link list rendering

- fold the duplicated <ul> list markup in the grouped and flat Apps
  branches into a single renderAppNavList helper so the list styling
  cannot drift between the two render paths

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
2026-07-16 11:24:55 -04:00

61 lines
1.5 KiB
Go

package handlers
import (
"encoding/json"
"strings"
"testing"
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
"github.com/Silo-Server/silo-server/internal/plugins"
)
func TestToUserPluginSettingsSummaryIncludesManifestCategory(t *testing.T) {
t.Parallel()
installation := &plugins.Installation{
ID: 42,
PluginID: "example-audiobooks",
Version: "1.2.3",
}
manifest := &pluginv1.PluginManifest{
Category: "Tools/Utilities",
}
got := toUserPluginSettingsSummary(installation, manifest)
if got.Category != "Tools/Utilities" {
t.Fatalf("Category = %q, want %q", got.Category, "Tools/Utilities")
}
if got.ID != 42 || got.PluginID != "example-audiobooks" || got.Version != "1.2.3" {
t.Fatalf("identity fields = %#v", got)
}
}
func TestToUserPluginSettingsSummaryOmitsEmptyCategory(t *testing.T) {
t.Parallel()
installation := &plugins.Installation{
ID: 7,
PluginID: "example-plain",
Version: "0.1.0",
}
got := toUserPluginSettingsSummary(installation, &pluginv1.PluginManifest{})
if got.Category != "" {
t.Fatalf("Category = %q, want empty", got.Category)
}
// The field is additive-only; ensure it stays absent from the JSON
// payload for manifests without a category so existing clients see an
// unchanged response shape.
encoded, err := json.Marshal(got)
if err != nil {
t.Fatalf("marshaling summary: %v", err)
}
if strings.Contains(string(encoded), "\"category\"") {
t.Fatalf("JSON payload should omit empty category, got %s", encoded)
}
}